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