defmodule Fussy.Validators.List do
@behaviour Fussy.Validator
alias Fussy.Utils
alias Fussy.ValidationError
defstruct [:inner, :min_length, :max_length]
@opaque t :: %__MODULE__{}
@spec new(
Fussy.Validator.t(),
min_length: non_neg_integer(),
max_length: non_neg_integer()
) :: __MODULE__.t()
def new(inner, opts \\ []) do
struct!(%__MODULE__{}, Keyword.merge(opts, inner: inner))
end
def validate(%__MODULE__{} = v, term), do: validate(v, [], term)
@impl true
def validate(
%__MODULE__{inner: inner, min_length: min_length, max_length: max_length},
path,
term
)
when is_list(term) do
[min_length: min_length, max_length: max_length]
|> Enum.filter(fn
{_, nil} -> false
_ -> true
end)
|> Enum.reduce([], fn validator, errors ->
case validate_with(validator, term) do
:ok ->
errors
msg ->
[%ValidationError{path: path, mod: __MODULE__, msg: msg, term: term} | errors]
end
end)
|> then(fn
[] -> validate_inner(inner, path, term)
errors -> {:error, errors}
end)
end
@impl true
def validate(_, path, term),
do:
{:error, [%ValidationError{path: path, mod: __MODULE__, msg: "must be a list", term: term}]}
defp validate_inner(inner, path, list) do
list
|> Enum.with_index()
|> Enum.reduce({[], []}, fn {item, idx}, {valid_list, errors} ->
case Utils.validate_using(inner, path ++ [idx], item) do
{:ok, item} ->
{[item | valid_list], errors}
{:error, inner_errors} ->
{valid_list, errors ++ inner_errors}
end
end)
|> then(fn
{valid_list, []} -> {:ok, valid_list |> Enum.reverse()}
{_, errors} -> {:error, errors}
end)
end
defp validate_with({:min_length, min_length}, list) do
len = length(list)
if len >= min_length do
:ok
else
"must have at least #{min_length} item(s), found #{len}"
end
end
defp validate_with({:max_length, max_length}, list) do
len = length(list)
if len <= max_length do
:ok
else
"must have at most #{max_length} item(s), found #{len}"
end
end
end