lib/exshome/tag/mapping.ex

defmodule Exshome.Tag.Mapping do
  @moduledoc """
  Computes the tag mapping.
  """

  @enforce_keys [:type, :key, :value]
  defstruct [:type, :key, :child_key, :value]

  @type t() :: %__MODULE__{
          type: :simple | :nested_atom_map | :nested_binary_map,
          key: any(),
          child_key: String.t() | atom(),
          value: module()
        }

  def compute_tag_mapping(params) do
    tag_data =
      for {module, tags} <- params,
          tag <- tags do
        to_tag_data(module, tag)
      end

    tag_data = Enum.group_by(tag_data, & &1.key)

    for {key, values} <- tag_data, into: %{} do
      nested_values =
        values
        |> validate_partial_mapping(key)
        |> values_to_mapping()

      {key, nested_values}
    end
  end

  defp to_tag_data(module, {tag, []}) do
    %__MODULE__{type: :simple, key: tag, value: module}
  end

  defp to_tag_data(module, {parent_key, [key: child_key]}) when is_atom(child_key) do
    %__MODULE__{
      type: :nested_atom_map,
      key: parent_key,
      child_key: child_key,
      value: module
    }
  end

  defp to_tag_data(module, {parent_key, [key: child_key]}) when is_binary(child_key) do
    %__MODULE__{
      type: :nested_binary_map,
      key: parent_key,
      child_key: child_key,
      value: module
    }
  end

  def validate_partial_mapping([%__MODULE__{type: type} | _] = values, key) do
    case Enum.uniq_by(values, & &1.type) do
      [_single_type] ->
        :ok

      data ->
        modules = Enum.map(data, & &1.value)
        raise "#{key} has mixed types in modules: #{inspect(modules)}"
    end

    duplicate_values = duplicated_by(values, :value)

    unless duplicate_values == [] do
      raise "#{key} has duplicate values: #{inspect(duplicate_values)}"
    end

    unless type == :simple do
      duplicate_keys = duplicated_by(values, :child_key)

      unless duplicate_keys == [] do
        raise "#{key} has duplicate keys: #{inspect(duplicate_keys)}"
      end
    end

    values
  end

  defp duplicated_by(values, field) do
    values
    |> Enum.frequencies_by(fn value -> Map.from_struct(value)[field] end)
    |> Enum.filter(&(elem(&1, 1) > 1))
    |> Enum.map(&elem(&1, 0))
  end

  def values_to_mapping([%__MODULE__{type: :simple} | _] = values) do
    values |> Enum.map(& &1.value) |> MapSet.new()
  end

  def values_to_mapping([%__MODULE__{type: type} | _] = values)
      when type in [:nested_atom_map, :nested_binary_map] do
    values |> Enum.map(&{&1.child_key, &1.value}) |> Enum.into(%{})
  end
end