lib/change_builders/full_diff/helpers.ex

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