lib/fussy/validators/integer.ex

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

  alias Fussy.ValidationError

  defstruct strict: true,
            gt: nil,
            lt: nil,
            gte: nil,
            lte: nil,
            multiple_of: nil

  @opaque t :: %__MODULE__{}

  @spec new(
          strict: boolean(),
          gt: number() | nil,
          lt: number() | nil,
          gte: number() | nil,
          lte: number() | nil,
          multiple_of: integer() | nil
        ) :: __MODULE__.t()
  def new(args \\ []), do: struct!(%__MODULE__{}, args)

  def validate(opts, term), do: validate(opts, [], term)

  @impl true
  def validate(%__MODULE__{strict: false} = v, path, term) when is_float(term) do
    validate(v, path, trunc(term), term)
  end

  @impl true
  def validate(%__MODULE__{} = v, path, term) when is_integer(term) do
    validate(v, path, term, term)
  end

  @impl true
  def validate(%__MODULE__{strict: false} = v, path, term) when is_binary(term) do
    case Integer.parse(term) do
      {n, ""} ->
        validate(v, path, n, term)

      {_, _} ->
        {:error,
         [%ValidationError{path: path, mod: __MODULE__, msg: "must be an integer", term: term}]}

      :error ->
        {:error,
         [%ValidationError{path: path, mod: __MODULE__, msg: "must be an integer", term: term}]}
    end
  end

  @impl true
  def validate(%__MODULE__{}, path, term),
    do:
      {:error,
       [%ValidationError{path: path, mod: __MODULE__, msg: "must be an integer", term: term}]}

  def validate(
        %__MODULE__{gt: gt, lt: lt, gte: gte, lte: lte, multiple_of: multiple_of},
        path,
        term,
        original_term
      )
      when is_integer(term) do
    [gt: gt, lt: lt, gte: gte, lte: lte, multiple_of: multiple_of]
    |> Enum.filter(fn
      {_, nil} -> false
      _ -> true
    end)
    |> Enum.reduce([], fn validator, errors ->
      case validate_with(validator, term) do
        :ok -> errors
        reason -> [reason | errors]
      end
    end)
    |> then(fn
      [] ->
        {:ok, term}

      errors ->
        {:error,
         errors
         |> Enum.map(fn msg ->
           %ValidationError{path: path, mod: __MODULE__, msg: msg, term: original_term}
         end)}
    end)
  end

  defp validate_with({:gt, gt}, n) when n > gt, do: :ok
  defp validate_with({:gt, gt}, _), do: "must be greater than #{gt}"

  defp validate_with({:gte, gte}, n) when n >= gte, do: :ok
  defp validate_with({:gte, gte}, _), do: "must be greater than or equal to #{gte}"

  defp validate_with({:lt, lt}, n) when n < lt, do: :ok
  defp validate_with({:lt, lt}, _), do: "must be less than #{lt}"

  defp validate_with({:lte, lte}, n) when n <= lte, do: :ok
  defp validate_with({:lte, lte}, _), do: "must be less than or equal to #{lte}"

  defp validate_with({:multiple_of, multiple_of}, n) do
    if Integer.mod(n, multiple_of) == 0 do
      :ok
    else
      "must be a multiple of #{multiple_of}"
    end
  end
end