defmodule Parameter.Validators do
  @moduledoc """
  Common validators to use within fields of the `Parameter.Schema`

  ## Examples

      param User do
        field :email, :string, validator: &Validators.email(&1)
      end

      iex> Parameter.load(User, %{"email" => "not an email"})
      {:error, %{email: "is invalid"}}
  """

  @type resp :: :ok | {:error, String.t()}
  @email_regex ~r/^[A-Za-z0-9\._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/

  @doc """
  Validates email address
      param User do
        field :email, :string, validator: &Validators.email(&1)
      end

      iex> Parameter.load(User, %{"email" => "not an email"})
      {:error, %{email: "is invalid"}}

      iex> Parameter.load(User, %{"email" => "john@gmail.com"})
      {:ok, %{email: "john@gmail.com"}}
  """
  @spec email(binary()) :: resp
  def email(value) when is_binary(value) do
    if String.match?(value, @email_regex) do
      :ok
    else
      error_tuple()
    end
  end

  @doc """
  Validates if value is equal to another
      param User do
        field :permission, :string, validator: {&Validators.equal/2, to: "admin"}
      end

      iex> Parameter.load(User, %{"permission" => "super_admin"})
      {:error, %{permission: "is invalid"}}

      iex> Parameter.load(User, %{"permission" => "admin"})
      {:ok, %{permission: "admin"}}
  """
  @spec equal(binary(), to: any()) :: resp
  def equal(value, to: comparable) do
    if value == comparable do
      :ok
    else
      error_tuple()
    end
  end

  @doc """
  Validates if a value is between a min and max
      param User do
        field :age, :integer, validator: {&Validators.length/2, min: 18, max: 50}
      end

      iex> Parameter.load(User, %{"age" => 12})
      {:error, %{age: "is invalid"}}

      iex> Parameter.load(User, %{"age" => 30})
      {:ok, %{age: 30}}
  """
  @spec length(binary(), Keyword.t()) :: resp
  def length(value, min: min, max: max) do
    case length(value, min: min) do
      {:error, _reason} = error -> error
      :ok -> length(value, max: max)
    end
  end

  def length(value, min: min) do
    if value >= min do
      :ok
    else
      error_tuple()
    end
  end

  def length(value, max: max) do
    if value <= max do
      :ok
    else
      error_tuple()
    end
  end

  @doc """
  Validates if a value is a member of the list
      param User do
        field :permission, :atom, validator: {&Validators.one_of/2, options: [:admin, :super_admin]}
      end

      iex> Parameter.load(User, %{"permission" => "normal_user"})
      {:error, %{permission: "is invalid"}}

      iex> Parameter.load(User, %{"permission" => "super_admin"})
      {:ok, %{permission: :super_admin}}
  """
  @spec one_of(binary(), options: any()) :: resp
  def one_of(value, options: options) when is_list(options) do
    if value in options do
      :ok
    else
      error_tuple()
    end
  end

  @doc """
  Validates if a value is not a member
      param User do
        field :permission, :atom, validator: {&Validators.none_of/2, options: [:admin, :super_admin]}
      end

      iex> Parameter.load(User, %{"permission" => "super_admin"})
      {:error, %{permission: "is invalid"}}

      iex> Parameter.load(User, %{"permission" => "normal_user"})
      {:ok, %{permission: :normal_user}}
  """
  @spec none_of(binary(), options: any()) :: resp
  def none_of(value, options: options) do
    case one_of(value, options: options) do
      :ok -> error_tuple()
      _error -> :ok
    end
  end

  @doc """
  Validates if a value matches a regex expression
      param User do
        field :code, :string, validator: {&Validators.regex/2, regex: ~r/code/}
      end

      iex> Parameter.load(User, %{"code" => "12345"})
      {:error, %{code: "is invalid"}}

      iex> Parameter.load(User, %{"code" => "code:12345"})
      {:ok, %{code: "code:12345"}}
  """
  @spec regex(binary(), regex: any()) :: resp
  def regex(value, regex: regex) when is_binary(value) do
    if String.match?(value, regex) do
      :ok
    else
      error_tuple()
    end
  end

  defp error_tuple do
    {:error, "is invalid"}
  end
end
