lib/fussy/validators/atom.ex

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

  alias Fussy.ValidationError

  defstruct [:atom, :case_sensitive]

  @opaque t :: %__MODULE__{}

  @spec new(atom(), case_sensitive: bool()) :: __MODULE__.t()
  def new(atom, args \\ []), do: struct!(%__MODULE__{atom: atom}, args)

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

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

  @impl true
  def validate(%__MODULE__{atom: atom, case_sensitive: false} = v, path, term)
      when is_binary(term) do
    if String.downcase(term) == Atom.to_string(atom) |> String.downcase() do
      {:ok, atom}
    else
      error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{atom: atom, case_sensitive: false} = v, path, term)
      when is_atom(term) do
    if Atom.to_string(term) |> String.downcase() == Atom.to_string(atom) |> String.downcase() do
      {:ok, atom}
    else
      error(v, path, term)
    end
  end

  @impl true
  def validate(%__MODULE__{atom: atom} = v, path, term) when is_binary(term) do
    if atom == String.to_existing_atom(term) do
      {:ok, atom}
    else
      error(v, path, term)
    end
  rescue
    _ in ArgumentError -> error(v, path, term)
  end

  @impl true
  def validate(%__MODULE__{} = v, path, term), do: error(v, path, term)

  defp error(%__MODULE__{atom: atom}, path, term) do
    {:error,
     [
       %ValidationError{
         mod: __MODULE__,
         path: path,
         msg: "must be #{inspect(atom)} or \"#{atom}\"",
         term: term
       }
     ]}
  end
end