defmodule AshPaperTrail.ChangeBuilders.FullDiff.Helpers do @moduledoc """ Misc helpers for building a full diff of a changeset. """ def dump_value(nil, _attribute), do: nil def dump_value(values, %{type: {:array, attr_type}} = attribute) do item_constraints = attribute.constraints[:items] # This is a work around for a bug in Ash.Type.dump_to_embedded/3 Enum.map(values, fn value -> {:ok, dumped_value} = Ash.Type.dump_to_embedded(attr_type, value, item_constraints) dumped_value end) end def dump_value(value, attribute) do {:ok, dumped_value} = Ash.Type.dump_to_embedded(attribute.type, value, attribute.constraints) dumped_value end @doc """ Builds a simple change map based on the given values. attribute_change_map({data_present, data, value_present, value}) """ def attribute_change_map({false, _data, _, value}), do: %{to: value} def attribute_change_map({true, data, false, _}), do: %{unchanged: data} def attribute_change_map({true, data, true, data}), do: %{unchanged: data} def attribute_change_map({true, data, true, value}), do: %{from: data, to: value} def is_union?(type) do type == Ash.Type.Union or (Ash.Type.NewType.new_type?(type) && Ash.Type.NewType.subtype_of(type) == Ash.Type.Union) end def is_embedded?(type), do: Ash.Type.embedded_type?(type) def embedded_union?(type, subtype) do with true <- is_union?(type), true <- :erlang.function_exported(type, :subtype_constraints, 0), subtype_constraints <- type.subtype_constraints(), subtypes when not is_nil(subtypes) <- Keyword.get(subtype_constraints, :types), subtype_config when not is_nil(subtype) <- Keyword.get(subtypes, subtype), subtype_config_type when not is_nil(subtype_config_type) <- Keyword.get(subtype_config, :type) do is_embedded?(subtype_config_type) else _ -> false end end @doc """ Building a map of attribute changes for the embedded resource """ def attribute_changes(%{} = data_map, nil) do for key <- keys_in([data_map]), into: %{}, do: {key, %{from: Map.get(data_map, key)}} end def attribute_changes(%{} = data_map, %{} = value_map) do for key <- keys_in([data_map, value_map]), into: %{}, do: attribute_change(key, data_map, value_map) end defp attribute_change(key, data_map, value_map) do {data_present, dumped_data} = map_key(data_map, key) {value_present, dumped_value} = map_key(value_map, key) change = attribute_change_map({data_present, dumped_data, value_present, dumped_value}) {key, change} end defp keys_in(map_list) do Enum.reduce(map_list, MapSet.new(), fn map, keys -> Map.keys(map) |> MapSet.new() |> MapSet.union(keys) end) end defp map_key(%{} = map, key) do {Map.has_key?(map, key), Map.get(map, key)} end # returns a list of primary keys for the given resource, or nil if there are none def unique_id(%Ash.Union{value: %{__struct__: _} = value}, dumped_value), do: unique_id(value, dumped_value) def unique_id(%Ash.Union{}, dumped_value), do: dumped_value def unique_id(nil, _dumped_value), do: nil def unique_id(%{__struct__: resource}, dump_value) do case Ash.Resource.Info.primary_key(resource) do [] -> nil primary_keys -> Enum.reduce(primary_keys, [resource], &(&2 ++ [Map.get(dump_value, &1)])) end end def unique_id(simple_value, _dump_value), do: simple_value def build_index_change(nil, to), do: %{to: to} def build_index_change(from, nil), do: %{from: from} def build_index_change(from, from), do: %{unchanged: from} def build_index_change(from, to), do: %{from: from, to: to} def map_get_keys(resource, keys) do Enum.map(keys, &Map.get(resource, &1)) end # Builds a simple change map based on the given values. # # change_map({data_present, data, value_present, value}) def embedded_change_map({:not_present, :not_present}), do: %{to: nil} def embedded_change_map({:not_present, nil}), do: %{to: nil} def embedded_change_map({:not_present, {_uid, %{} = value}}), do: %{created: attribute_changes(%{}, value)} def embedded_change_map({nil, :not_present}), do: %{unchanged: nil} def embedded_change_map({nil, nil}), do: %{unchanged: nil} def embedded_change_map({nil, {_uid, %{} = value}}), do: %{created: attribute_changes(%{}, value), from: nil} def embedded_change_map({{_uid, data}, :not_present}), do: %{unchanged: attribute_changes(data, data)} def embedded_change_map({{_uid, data}}), do: %{destroyed: attribute_changes(data, nil)} def embedded_change_map({{_uid, data}, nil}), do: %{destroyed: attribute_changes(data, nil), to: nil} def embedded_change_map({{nil, data}, {nil, value}}), do: %{destroyed: attribute_changes(data, nil), created: attribute_changes(%{}, value)} def embedded_change_map({{uid, data}, {uid, data}}), do: %{unchanged: attribute_changes(data, data)} def embedded_change_map({{uid, data}, {uid, value}}), do: %{updated: attribute_changes(data, value)} def embedded_change_map({{_data_pk, data}, {_value_pk, value}}), do: %{destroyed: attribute_changes(data, nil), created: attribute_changes(%{}, value)} # def union_change_map({{_data_present, _data_type, _data}, { _value_present, _value_type, _value}}), # Non-present to still no value def union_change_map({:not_present, :not_present}), do: %{to: nil} # Non-present to nil def union_change_map({:not_present, {:non_embedded, nil, nil}}), do: %{to: nil} # Not present to non_embedded def union_change_map({:not_present, {:non_embedded, type, value}}), do: %{to: %{type: to_string(type), value: value}} # Not present to embedded def union_change_map({:not_present, {:embedded, type, _uid, value}}), do: %{to: %{type: to_string(type), created: attribute_changes(%{}, value)}} # nil unchanged def union_change_map({{:non_embedded, nil, nil}, :not_present}), do: %{unchanged: nil} # nil to nil def union_change_map({{:non_embedded, nil, nil}, {:non_embedded, nil, nil}}), do: %{unchanged: nil} # nil to embedded def union_change_map({{:non_embedded, nil, nil}, {:embedded, type, _uid, value}}), do: %{ from: nil, to: %{type: to_string(type), created: attribute_changes(%{}, value)} } # nil to non_embedded def union_change_map({{:non_embedded, nil, nil}, {:non_embedded, type, value}}), do: %{ from: nil, to: %{type: to_string(type), value: value} } # non_embedded to not present def union_change_map({{:non_embedded, type, data}, :not_present}), do: %{unchanged: %{type: to_string(type), value: data}} def union_change_map({{:non_embedded, type, data}, :removed}), do: %{ from: %{type: to_string(type), value: data} } # non_embedded to nil def union_change_map({{:non_embedded, type, data}, {:non_embedded, nil, nil}}), do: %{ from: %{type: to_string(type), value: data}, to: nil } # non_embedded to same non_embedded def union_change_map({{:non_embedded, type, data}, {:non_embedded, type, data}}), do: %{unchanged: %{type: to_string(type), value: data}} # non_embedded to different non_embedded def union_change_map({{:non_embedded, data_type, data}, {:non_embedded, value_type, value}}), do: %{ from: %{type: to_string(data_type), value: data}, to: %{type: to_string(value_type), value: value} } # non_embedded to embedded def union_change_map({{:non_embedded, data_type, data}, {:embedded, value_type, _pk, value}}), do: %{ from: %{type: to_string(data_type), value: data}, to: %{type: to_string(value_type), created: attribute_changes(%{}, value)} } # embedded to not present def union_change_map({{:embedded, type, _pk, data}, :not_present}), do: %{ unchanged: %{type: to_string(type), value: attribute_changes(data, data)} } # embedded to removed def union_change_map({{:embedded, type, _pk, data}, :removed}), do: %{ from: %{ type: to_string(type), destroyed: attribute_changes(data, nil) } } # embedded to nil def union_change_map({{:embedded, type, _pk, data}, {:non_embedded, nil, nil}}), do: %{ from: %{ type: to_string(type), destroyed: attribute_changes(data, nil) }, to: nil } # embedded to non_embedded def union_change_map({{:embedded, data_type, _pk, data}, {:non_embedded, value_type, value}}), do: %{ from: %{ type: to_string(data_type), destroyed: attribute_changes(data, nil) }, to: %{type: to_string(value_type), value: value} } # embedded to same embedded def union_change_map({{:embedded, type, pk, data}, {:embedded, type, pk, data}}), do: %{ unchanged: %{ type: to_string(type), value: attribute_changes(data, data) } } # embedded to updated embedded def union_change_map({{:embedded, type, pk, data}, {:embedded, type, pk, value}}), do: %{ updated: %{ type: to_string(type), value: attribute_changes(data, value) } } # embedded to different embedded def union_change_map( {{:embedded, data_type, _data_pk, data}, {:embedded, value_type, _value_pk, value}} ), do: %{ from: %{ type: to_string(data_type), destroyed: attribute_changes(data, nil) }, to: %{type: to_string(value_type), created: attribute_changes(%{}, value)} } end