lib/fussy/validators/fixed_map.ex

defmodule Fussy.Validators.FixedMap do
  @behaviour Fussy.Validator

  alias Fussy.Utils

  defstruct [:fixed_map, :aliases]

  @opaque t :: %__MODULE__{}

  @spec new(%{atom() => Fussy.Validator.t()}) :: __MODULE__.t()
  def new(fixed_map, opts \\ []) when is_map(fixed_map) and is_list(opts) do
    %__MODULE__{
      fixed_map: fixed_map,
      aliases: Keyword.get(opts, :aliases, %{})
    }
  end

  @impl true
  def validate(%__MODULE__{} = v, maybe_kwlist) when is_list(maybe_kwlist) do
    if Keyword.keyword?(maybe_kwlist) do
      validate(v, Map.new(maybe_kwlist))
    else
      {:error, ["must be a map or keyword list"]}
    end
  end

  @impl true
  def validate(%__MODULE__{fixed_map: fixed_map, aliases: aliases}, map) when is_map(map) do
    fixed_map
    |> Enum.reduce({%{}, []}, fn {key, value_validator}, {acc, errors} ->
      unvalidated_value =
        Map.get_lazy(map, key, fn ->
          Map.get_lazy(map, Atom.to_string(key), fn ->
            case Map.get(aliases, key, key) do
              nil ->
                nil

              aliased_key ->
                Map.get_lazy(map, aliased_key, fn ->
                  Map.get(map, Atom.to_string(aliased_key))
                end)
            end
          end)
        end)

      case Utils.validate_using(value_validator, unvalidated_value) do
        {:ok, value} ->
          {Map.put(acc, key, value), errors}

        {:error, reason} ->
          {acc, ["#{inspect(key)} => {#{reason |> Enum.join(", ")}}" | errors]}
      end
    end)
    |> then(fn
      {map, []} ->
        {:ok, map}

      {_, errors} ->
        {:error, Enum.reverse(errors)}
    end)
  end

  @impl true
  def validate(_, _), do: {:error, ["must be a map or keyword list"]}
end