lib/validate.ex

defmodule Fields.Validate do
  @moduledoc """
  Helper functions to validate the data in certain fields
  """

  @doc """
  Validate an address.
  Currently just validates that some input has been given.
  """
  def address(address) do
    String.length(address) > 0
  end

  @doc """
  Validate the format of an email address using a regex.
  Uses a slightly modified version of the w3c HTML5 spec email regex (https://www.w3.org/TR/html5/forms.html#valid-e-mail-address),
  with additions to account for not allowing emails to start or end with '.',
  and a check that there are no consecutive '.'s.
  """
  def email(email) do
    {:ok, regex} =
      Regex.compile(
        "^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]*(?<!\\.)@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
      )

    Regex.match?(regex, email) && !String.contains?(email, "..")
  end

  @doc """
  Validate the length of a name is less than 35 characters
  """
  def name(name) do
    len = String.length(name)
    1 < len && len < 35
  end

  @doc """
  Validates the format of a UK phone number.
  """
  def phone_number(phone) do
    regex =
      ~r/^(((\+44\s?\d{4}|\(?0\d{4}\)?)\s?\d{3}\s?\d{3})|((\+44\s?\d{3}|\(?0\d{3}\)?)\s?\d{3}\s?\d{4})|((\+44\s?\d{2}|\(?0\d{2}\)?)\s?\d{4}\s?\d{4}))(\s?\#(\d{4}|\d{3}))?$/

    Regex.match?(regex, phone)
  end

  @doc """
  Validate the format of an postcode using a regex.
  All existing postcodes in the UK should pass this validation;
  some non-existent ones may too if they follow the standard UK postcode format.
  """
  def postcode(postcode) do
    {:ok, regex} =
      Regex.compile(
        "^([A-Za-z][A-Za-z]?[0-9][A-Za-z0-9]? ?[0-9][A-Za-z]{2}|[Gg][Ii][Rr] ?0[Aa]{2})$"
      )

    Regex.match?(regex, postcode)
  end

  @doc """
  Validate the format of a url using a regex.
  See https://stackoverflow.com/questions/42522442/how-to-validate-a-url-via-a-check-constraint-in-postgres/51965621 for details on how the regex for validation was chosen
  """
  def url(url) do
    regex = ~r/(?:https?|ftp):\/\/(www\\.)?[-a-zA-Z0-9@:%._\+~#=]{2,255}\.[a-z]{2,9}\b([-a-zA-Z0-9@:%_+.,~#?!&>\/\/=]*)$/
    Regex.match?(regex, url)
  end

  def ip_address(ip_address) do
    # https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
    {:ok, regex_ipv6} =
      Regex.compile(
        "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"
      )

    # https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
    {:ok, regex_ipv4} = Regex.compile("((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}")
    Regex.match?(regex_ipv6, ip_address) || Regex.match?(regex_ipv4, ip_address)
  end
end