lib/fussy/validators/fixed_map.ex

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

  alias Fussy.Utils
  alias Fussy.ValidationError

  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

  def validate(%__MODULE__{} = v, term), do: validate(v, [], term)

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

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

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

      case Utils.validate_using(value_validator, path ++ [key], unvalidated_value) do
        {:ok, value} ->
          {Map.put(map, key, value), errors}

        {:error, inner_errors} ->
          {map, errors ++ inner_errors}
      end
    end)
    |> then(fn
      {map, []} -> {:ok, map}
      {_, errors} -> {:error, errors}
    end)
  end

  @impl true
  def validate(%__MODULE__{}, path, term),
    do:
      {:error,
       [
         %ValidationError{
           mod: __MODULE__,
           path: path,
           msg: "must be a map or keyword list",
           term: term
         }
       ]}
end