lib/fussy/validators/tuple.ex

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

  alias Fussy.Utils

  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

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

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

      case Utils.validate_using(validator, item) do
        {:ok, valid_item} ->
          {Tuple.append(valid_tuple, valid_item), errors}

        {:error, reason} ->
          {valid_tuple, ["idx #{idx} => {#{reason |> Enum.join(", ")}}" | errors]}
      end
    end)
    |> then(fn
      {valid_tuple, []} -> {:ok, valid_tuple}
      {_, errors} -> {:error, errors |> Enum.reverse()}
    end)
  end

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

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