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