lib/fussy/validators/tuple.ex

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

  alias Fussy.Utils
  alias Fussy.ValidationError

  defstruct inner: nil, strict: true

  @opaque t :: %__MODULE__{}

  @spec new(tuple(), strict: boolean()) :: __MODULE__.t()
  def new(inner, args \\ []) when is_tuple(inner) do
    struct!(%__MODULE__{}, Keyword.merge(args, inner: inner))
  end

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

  @impl true
  def validate(%__MODULE__{inner: {}}, _path, {}), do: {:ok, {}}

  @impl true
  def validate(%__MODULE__{inner: inner}, path, term)
      when is_tuple(term) and tuple_size(inner) == tuple_size(term) do
    0..(tuple_size(inner) - 1)
    |> Enum.reduce({{}, []}, fn idx, {tuple, errors} ->
      validator = elem(inner, idx)
      item = elem(term, idx)

      case Utils.validate_using(validator, path ++ [idx], item) do
        {:ok, valid_item} ->
          {Tuple.append(tuple, valid_item), errors}

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

      {_, errors} ->
        {:error, errors}
    end)
  end

  @impl true
  def validate(%__MODULE__{inner: inner, strict: false} = v, path, list)
      when is_list(list) and tuple_size(inner) == length(list) do
    validate(v, path, List.to_tuple(list))
  end

  @impl true
  def validate(%__MODULE__{inner: inner}, path, term),
    do:
      {:error,
       [
         %ValidationError{
           mod: __MODULE__,
           msg: "must be a tuple of size #{tuple_size(inner)}",
           path: path,
           term: term
         }
       ]}
end