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, 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),
do:
Keyword.pop(validators, :message)
|> (&reverse_tuple/1).()
|> (&validate(data, position, &1)).()
@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