lib/validators/at.ex

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

  @moduledoc """

  Run validator for an element at a given position.

  > #### Validate other types of data {: .tip}
  >
  > By default, String, and List are accepted, but other types can be added by implementing the `ValueAt` protocol for this type.

  To validate the third letter in the string, run:

      iex> Dsv.At.validate("string to validate", "2", equal: "r")
      :ok

  Elements are numbered as in the elixir List, starting from 0.

  """

  message(&get_errors/3)

  @doc """
  Run validator for an element at a given position.


  ## Example
      Validator for string and list. Run all validators against element on provided position.

      iex> Dsv.At.valid?("abcd", "2", format: ~r/^[a-z]$/, equal: "w")
      :false

      iex> Dsv.At.valid?("abcd", "2", format: ~r/^[a-z]$/, equal: "c")
      :true

      iex> Dsv.At.valid?("abcd", "2", format: ~r/^[k-z]$/, equal: "a")
      :false

      iex> Dsv.At.valid?("abcd", "2", format: ~r/^[a-d]$/, custom: [function: &is_bitstring/1])
      :true

      iex> Dsv.At.valid?("abcd", "2", format: ~r/^[g-j]$/, custom: [function: &is_bitstring/1])
      :false


      iex> Dsv.At.valid?(["test", 1, ~D[2001-11-10]], "2", date: [min: ~D[1999-10-09], max: ~D[2020-10-10]])
      :true

      iex> Dsv.At.valid?(["test", 1, ~D[2001-11-10]], "2", date: [min: ~D[2002-10-09], max: ~D[2020-10-10]])
      :false

  """
  def valid?(data, options, binded_values) when is_list(options) and is_map(binded_values),
    do: super(data, options, binded_values)

  def valid?(data, position, options), do: get_element(data, position) |> Dsv.valid?(options)

  @doc """
  Run validators for elements on specified positions.
  Get data to validate as the first argument and keyword list as a second.
  Keys in second arguments are positions of elements to validates and values are validators to run for those elements.

  ## Example

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[a-z]$/, equal: "w"])
      :false

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[a-z]$/, equal: "w"])
      :false

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[a-z]$/, equal: "c"])
      :true

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[k-z]$/, equal: "a"])
      :false

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[a-d]$/, custom: [function: &is_bitstring/1]])
      :true

      iex> Dsv.At.valid?("abcd", "2": [format: ~r/^[g-j]$/, custom: [function: &is_bitstring/1]])
      :false

      iex> Dsv.At.valid?("abcd", "0": [format: ~r/^[a-z]$/, equal: "c"], "2": [format: ~r/^[a-z]$/, equal: "c"], "3": [format: ~r/^[a-z]$/, equal: "w"])
      :false

      iex> Dsv.At.valid?("abcd", "0": [format: ~r/^[a-z]$/, equal: "c"], "2": [format: ~r/^[a-z]$/, equal: "c"], "3": [format: ~r/^[a-z]$/, equal: "w"])
      :false


  """
  def valid?(data, options), do: super(data, options)

  @doc """
  Run validator for an element at a given position.

  ## Example
      Validator for string and list. Run all validators against element on provided position.

      iex> Dsv.At.validate("abcd", "2", format: ~r/^[a-z]$/, equal: "w")
      {:error, %{:"2" =>  ["Values must be equal"]}}

      iex> Dsv.At.validate("abcd", "2", format: ~r/^[a-z]$/, equal: "c")
      :ok

      iex> Dsv.At.validate("abcd", "2", format: ~r/^[k-z]$/, equal: "a")
      {:error, %{:"2" =>  ["Value c does not match pattern ^[k-z]$", "Values must be equal"]}}

      iex> Dsv.At.validate("abcd", "2", format: ~r/^[a-d]$/, custom: [function: &is_bitstring/1, message: "Data must be a bitstring"])
      :ok

      iex> Dsv.At.validate("abcd", "2", format: ~r/^[g-j]$/, custom: [function: &is_bitstring/1, message: "Data must be a bitstring"])
      {:error, %{:"2" =>  ["Value c does not match pattern ^[g-j]$"]}}

      iex> Dsv.At.validate(["test", 1, ~D[2001-11-10]], "2", date: [min: ~D[1999-10-09], max: ~D[2020-10-10]])
      :ok

      iex> Dsv.At.validate(["test", 1, ~D[2001-11-10]], "2", date: [min: ~D[2002-10-09], max: ~D[2020-10-10]])
      {:error, %{:"2" =>  ["Date must be between 2002-10-09 and 2020-10-10"]}}

      iex> Dsv.At.validate("abcd", "0", format: ~r/^[a-z]$/, message: "First letter must be lowercase.")
      :ok

      iex> Dsv.At.validate("Abcd", "0", format: ~r/^[a-z]$/, message: "First letter must be lowercase.")
      {:error, "First letter must be lowercase."}

  """
  def validate(data, position, {validators, nil}),
    do: validate(data, [{String.to_atom(position), validators}])

  def validate(data, position, {validators, message}),
    do: validate(data, [{String.to_atom(position), validators}, {:message, message}])

  def validate(data, position, validators) when is_list(validators),
    do:
      Keyword.pop(validators, :message)
      |> (&reverse_tuple/1).()
      |> (&validate(data, position, &1)).()

  def validate(data, options, binded_values) when is_list(options) and is_map(binded_values) do
    super(data, options, binded_values)
  end

  @doc """
  Run validators for elements on specified positions.
  Get data to validate as the first argument and keyword list as a second.
  Keys in second arguments are positions of elements to validates and values are validators to run for those elements.

  ## Example

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[a-z]$/, equal: "w"])

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[a-z]$/, equal: "w"])
      {:error, %{:"2" =>  ["Values must be equal"]}}

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[a-z]$/, equal: "c"])
      :ok

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[k-z]$/, equal: "a"])
      {:error, %{:"2" =>  ["Value c does not match pattern ^[k-z]$", "Values must be equal"]}}

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[a-d]$/, custom: [function: &is_bitstring/1, message: "Data must be a bitstring"]])
      :ok

      iex> Dsv.At.validate("abcd", "2": [format: ~r/^[g-j]$/, custom: [function: &is_bitstring/1, message: "Data must be a bitstring"]])
      {:error, %{:"2" =>  ["Value c does not match pattern ^[g-j]$"]}}

      iex> Dsv.At.validate("abcd", "0": [format: ~r/^[a-z]$/, equal: "c"], "2": [format: ~r/^[a-z]$/, equal: "c"], "3": [format: ~r/^[a-z]$/, equal: "w"])
      {:error, %{:"0" =>   ["Values must be equal"], :"3" =>   ["Values must be equal"]}}

      iex> Dsv.At.validate("abcd",  "0": [format: ~r/^[a-z]$/, equal: "c"], "2": [format: ~r/^[a-z]$/, equal: "c"], "3": [format: ~r/^[a-z]$/, equal: "w"])
      {:error, %{:"0" =>   ["Values must be equal"], :"3" =>   ["Values must be equal"]}}

  """
  def validate(data, options), do: super(data, options)

  defp get_element(data, position) when is_integer(position), do: ValueAt.at(data, position)

  defp get_element(data, position) when is_bitstring(position),
    do: ValueAt.at(data, String.to_integer(position))

  defp get_element(data, position),
    do: ValueAt.at(data, String.to_integer(Atom.to_string(position)))

  defp reverse_tuple({first_elem, second_elem}), do: {second_elem, first_elem}
end