lib/fussy/validators/either.ex

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

  alias Fussy.Utils
  alias Fussy.ValidationError

  defstruct [:variants]

  @opaque t :: %__MODULE__{}

  @spec new(list(Fussy.Validator.t())) :: __MODULE__.t()

  def new([]), do: raise(ArgumentError, "must provide at least one variant")

  def new(variants) when is_list(variants) do
    %__MODULE__{variants: variants}
  end

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

  @impl true
  def validate(%__MODULE__{variants: variants}, path, term) do
    variants
    |> Enum.find_value(fn v ->
      case Utils.validate_using(v, term) do
        {:ok, valid_term} ->
          {:ok, valid_term}

        {:error, _} ->
          nil
      end
    end)
    |> then(fn
      {:ok, valid_term} -> {:ok, valid_term}
      nil -> error(path, term)
    end)
  end

  defp error(path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         path: path,
         msg: "must be one of the defined variants",
         term: term
       }
     ]}
  end
end