lib/fussy/validators/list.ex

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