lib/validators/format.ex

defmodule Veli.Validators.Format do
  @moduledoc """
  String format validator.
  
  ## Note
  Most of the format validation are done by using regex. Most of them are -> https://ihateregex.io/ <- taken from here.
  
  ## Atoms
  - `:email`: Email adress
  - `:url`: Url
  - `:slug`: Slug
  - `:ipv4`: IPv4
  - `:ipv6`: IPv6
  - `:ip`: both IPv4, IPv6
  - `:ascii`: ASCII
  - `:printable`: Printable ASCII
  - `:uuid`: UUID
  - `:mac`: MAC Adress
  - `:username`: Username
  - `:cc`: Credit Card
  - `:e164`: e.164 Phone Number Format
  - `:btcaddr`: Bitcoin Adress
  - `:semver`: Semantic Versioning
  
  ## Example
  
      rule = [type: :string, format: :url]
      Veli.valid("hello", rule) # not valid
      Veli.valid("https://x.org/", rule) # valid
  """

  @email_regex ~r/(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  @slug_regex ~r/^[a-z0-9]+(?:-[a-z0-9]+)*$/
  @ipv4_regex ~r/(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/
  @ipv6_regex ~r/(([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]))/
  @ascii_regex ~r/^[\x00-\x7F]+$/
  @printable_regex ~r/^[\x20-\x7E]+$/
  @uuid_regex ~r/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/
  @mac_regex ~r/^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$/
  @username_regex ~r/^[a-z0-9_-]+$/
  @cc_regex ~r/(^4[0-9]{12}(?:[0-9]{3})?$)|(^(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$)|(3[47][0-9]{13})|(^3(?:0[0-5]|[68][0-9])[0-9]{11}$)|(^6(?:011|5[0-9]{2})[0-9]{12}$)|(^(?:2131|1800|35\d{3})\d{11}$)/
  @e164_regex ~r/^\+[1-9]\d{1,14}$/
  @btcaddr_regex ~r/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/
  @semver_regex ~r/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/

  @spec valid?(binary, boolean) :: boolean
  def valid?(value, :email) when is_binary(value) do
    Regex.match?(@email_regex, value)
  end

  def valid?(value, :url) when is_binary(value) do
    uri = URI.parse(value)
    not is_nil(uri.scheme) and not is_nil(uri.host) and uri.host =~ "."
  end

  def valid?(value, :slug) when is_binary(value) do
    Regex.match?(@slug_regex, value)
  end

  def valid?(value, :ipv4) when is_binary(value) do
    Regex.match?(@ipv4_regex, value)
  end

  def valid?(value, :ipv6) when is_binary(value) do
    Regex.match?(@ipv6_regex, value)
  end

  def valid?(value, :ip) when is_binary(value) do
    Regex.match?(@ipv4_regex, value) or Regex.match?(@ipv6_regex, value)
  end

  def valid?(value, :ascii) when is_binary(value) do
    Regex.match?(@ascii_regex, value)
  end

  def valid?(value, :printable) when is_binary(value) do
    Regex.match?(@printable_regex, value)
  end

  def valid?(value, :uuid) when is_binary(value) do
    Regex.match?(@uuid_regex, value)
  end

  def valid?(value, :mac) when is_binary(value) do
    Regex.match?(@mac_regex, value)
  end

  def valid?(value, :username) when is_binary(value) do
    Regex.match?(@username_regex, value)
  end

  def valid?(value, :cc) when is_binary(value) do
    Regex.match?(@cc_regex, value)
  end

  def valid?(value, :e164) when is_binary(value) do
    Regex.match?(@e164_regex, value)
  end

  def valid?(value, :btcaddr) when is_binary(value) do
    Regex.match?(@btcaddr_regex, value)
  end

  def valid?(value, :semver) when is_binary(value) do
    Regex.match?(@semver_regex, value)
  end

  def valid?(_value, _rule) do
    false
  end
end