lib/validators/email.ex

defmodule Dsv.Email do
  use Dsv.Validator, complex: true

  @moduledoc """
  Dsv.Email module offers functions to validate an email address and optionally checks specific parts of it.

  > #### Notes: {: .info}
  >  This function uses basic email format validation and does not guarantee
  >  that the email address is deliverable or exists.

  """

  @email_regex ~r"\A(?<username>[\w!#$%&'*+/=?`{|}~^-]+(?:\.[\w!#$%&'*+/=?`{|}~^-]+)*)@(?<mail_server>[A-Z0-9-]+)\.+(?<top_level_domain>[A-Z]{2,6})\Z"i
  @top_level_domain "top_level_domain"
  @mail_server "mail_server"
  @username "username"

  message(&get_errors/3)

  @doc """
  The `valid?/2` function provides a robust mechanism to validate an email address
  while applying a set of associated validators based on provided criteria.

  ## Parameters

    * `email` (string) - The email address to be validated.
    * `options` (keyword list) - A keyword list containing validation criteria for specific parts of the email.
      Valid keys include:
      - `:username` - Validators for the username part of the email (before "@").
      - `:mail_server` - Validators for the mail server part of the email (between "@" and the last dot).
      - `:top_level_domain` - Validators for the top-level domain part of the email (after the last dot).


  ## Returns
  - `:true` Represents successful validation, where the email is correct and meets all the specified criteria.
  - `:false` if the data (email) fails validation.


  ## Example

      iex> Dsv.Email.valid?("mail.test@domain.com")
      :true

      iex> Dsv.Email.valid?("not-existing-email-address@domain.com")
      :true

      iex> Dsv.Email.valid?("this is not email address")
      :false

      iex> Dsv.Email.valid?("mail.test@domain.com", top_level_domain: [equal: "domain"], username: [format: ~r/m.*t.*/])
      :false

      iex> Dsv.Email.valid?("not-existing-email-address@domain.com", top_level_domain: [equal: "domain"], username: [format: ~r/m.*t.*/])
      :false

      iex> Dsv.Email.valid?("mail.test@domain.com", [])
      :true

      iex> Dsv.Email.valid?("this_is_not_real_email_address.com", top_level_domain: [equal: "com"])
      :false

  """
  def valid?(data, options \\ [])
  def valid?(data, []), do: Regex.match?(@email_regex, data)
  def valid?(data, options), do: valid?(data, []) and super(data, options)

  @doc """
  The `validate/2` function provides a robust mechanism to validate an email address
  while applying a set of associated validators based on provided criteria.

  ## Parameters

    * `email` (string) - The email address to be validated.
    * `options` (keyword list) - A keyword list containing validation criteria for specific parts of the email.
      Valid keys include:
      - `:username` - Validators for the username part of the email (before "@").
      - `:mail_server` - Validators for the mail server part of the email (between "@" and the last dot).
      - `:top_level_domain` - Validators for the top-level domain part of the email (after the last dot).
      - `:message` - Custom message returned in case of the validation failure.


  ## Returns
  - `:ok` - Represents successful validation, where the email is correct and meets all the specified criteria.
  - `{:error, errors}` - if the data (email) fails validation. The `errors` map provides detailed information about each part of the email that did not pass validation.

  ## Example

      iex> Dsv.Email.validate("mail.test@domain.com")
      :ok

      iex> Dsv.Email.validate("not-existing-email-address@domain.com")
      :ok

      iex> Dsv.Email.validate("this is not email address")
      {:error, "Invalid email address."}

      iex> Dsv.Email.validate("mail.test@domain.com", top_level_domain: [equal: "domain"], username: [format: ~r/m.*t.*/])
      {:error, %{top_level_domain: ["Values must be equal"]}}

      iex> Dsv.Email.validate("not-existing-email-address@domain.com", top_level_domain: [equal: "domain"], username: [format: ~r/m.*t.*/])
      {:error, %{top_level_domain: ["Values must be equal"], username: ["Value not-existing-email-address does not match pattern m.*t.*"]}}

      iex> Dsv.Email.validate("mail.test@domain.com", [])
      :ok

      iex> Dsv.Email.validate("this_is_not_real_email_address.com", top_level_domain: [equal: "com"])
      {:error, "Invalid email address."}

      iex> Dsv.Email.validate("mail.test@domain.com", top_level_domain: [equal: "domain"], username: [format: ~r/m.*t.*/], message: "Email address don't meet validation criteria.")
      {:error, "Email address don't meet validation criteria."}

  """
  def validate(data, []),
    do: if(valid?(data, []), do: :ok, else: {:error, "Invalid email address."})

  def validate(data, options),
    do: if(valid?(data, []), do: super(data, options), else: {:error, "Invalid email address."})

  defp get_element(data, :username),
    do: Regex.named_captures(@email_regex, data) |> Map.get(@username)

  defp get_element(data, :mail_server),
    do: Regex.named_captures(@email_regex, data) |> Map.get(@mail_server)

  defp get_element(data, :top_level_domain),
    do: Regex.named_captures(@email_regex, data) |> Map.get(@top_level_domain)
end