defmodule Dsv.Validator do
@moduledoc """
Validate user data
A behavior module for implementing a validator.
A Dsv.Validator is a module that defines a few useful macros that help when implementing a validator.
A Dsv.Validator provides `validate/2` function that will validate the input based on the function `valid?/2` that needs to be provided by the `Dsv.Validator` implementation.
A Dsv.Validator behavior abstracts the common validation functions and creation of error messages (in line with validator options provided by the user - more on that later).
A `Dsv.Validator` is a convenient way to create new validators which will handle error response generation and are a way to create validators that can be used with `Dsv.validate` function.
## Example
To create a simple validator, we need to create a new module that will use our `Dsv.Validator` behavior.
The only required thing to be implemented is the `valid?/2` function, which will get data to validate as a first argument and validator options as a second argument.
iex> defmodule Dsv.IsTrue do
...> use Dsv.Validator
...>
...> def valid?(data, options \\\\ []), do: if data == :true, do: :true, else: :false
...> end
This is the smallest working example of a new validator.
This can be used in a few ways.
The first way is to check if the value is valid using the `Dsv.IsTrue.valid?/2` function. This will return `:true` if `data` is `:true` and `:false` otherwise.
iex> Dsv.IsTrue.valid?(:true)
:true
iex> Dsv.IsTrue.valid?(:false)
:false
iex> Dsv.IsTrue.valid?("this is not true")
:false
Using `Dsv.Validator` provides another useful function, `Dsv.IsTrue.validate/2`. In this case, the result will be `:ok` in case the data is valid (`:true`) and `{:error, message}` in case the data is invalid (`:false`).
iex> Dsv.IsTrue.validate(:true)
:ok
iex> Dsv.IsTrue.validate(:false)
{:error, "Unknown validation error"}
On validation failure, we get a tuple with `:error` as the first element and the default error message "Unknown validation error". This error message is not very helpful if we are interested in the reason for the failure.
We can change it by defining error messages specific to the validator options.
We do not use validator options in the above example, so the error message definition will look like this (it will not be specific to validation options).
iex> defmodule Dsv.IsTrue do
...> use Dsv.Validator
...>
...> message "Provided value is invalid. Accepted value is ':true'"
...>
...> def valid?(data, options \\\\ []), do: if data == :true, do: :true, else: :false
...> end
iex> Dsv.IsTrue.validate(:false)
{:error, "Provided value is invalid. Accepted value is ':true'"}
When we define a validator that will get data to validate and validate options, we can create a message for each possible options combination.
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value is too short"}
...> message {:max, "Value is too long"}
...> message {[:min, :max], "Value is too short or too long"}
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
In this case, when we use `Dsv.StringLength.validate/2` function, we will get a message related to what we want to validate (options we provided).
iex> Dsv.StringLength.validate("test", min: 6)
{:error, "Value is too short"}
iex> Dsv.StringLength.validate("test", max: 2)
{:error, "Value is too long"}
iex> Dsv.StringLength.validate("test", min: 10, max: 100)
{:error, "Value is too short or too long"}
#### Defining messages with data and options information.
Besides simple messages like the above, we can create messages containing data under validation and options used during validation.
We can use EEx (Embedded Elixir) to create such a message.
We can use `data`, which contains data provided as the first argument to the `validate/2` function, and `options`, which contains data provided as the second argument to the `validate/2` function.
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
...> message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
...> message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
iex> Dsv.StringLength.validate("test", min: 6)
{:error, "Value 'test' is shorter than expected 6 characters."}
iex> Dsv.StringLength.validate("test", max: 2)
{:error, "Value 'test' is longer than expected 2 characters."}
iex> Dsv.StringLength.validate("test", min: 10, max: 100)
{:error, "Value 'test' is shorter than expected 10 or longer than expected 100 characters."}
When we expect the raise of an Exception during validation, we can catch that Exception and change it to the error message.
We must define the error message using macro `e_message/1` to do that.
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
...> message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
...> message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
...>
...> e_message "Unexpected error occured during validation. The data or options you provided are incorrect."
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
iex> Dsv.StringLength.validate(4443432, min: 6)
{:error, "Unexpected error occured during validation. The data or options you provided are incorrect."}
If the `e_message` macro is not used to define an error message, then `FunctionClauseError` will be returned in this example.
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
...> message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
...> message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
...>
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
iex> Dsv.StringLength.validate(4443432, min: 6)
** (FunctionClauseError) no function clasue matching in String.length/1
## Different ways of defining messages.
You can define validation messages in three different ways:
- Using message macro (as seen above) with the options value and the message as EEx string.
- Using function which will receive four arguments
See more in the `message/1` documentation.
#### Map options to the message keys
##### Default `message_key_mapper` implementation.
By default, `:message_key_mapper` will return a list of the option's keys.
The order of the options in the `validate` function is not essential.
This is used with a combination of `message` macro. Let's assume you defined a few messages like that:
## Example
iex> defmodule Dsv.Validator.MessageExample do
...> use Dsv.Validator
...>
...> message {:first, "This is the message produced when validator is call with option `:first`"}
...> message {:second, "This is the message produced when validator is call with option `:second`"}
...> message {:third, "This is the message produced when validator is call with option `:third`"}
...> message {[:first, :second, :third], "This is the message produced when validator is call with all of the options `:first`, `:second`, `:third`"}
...>
...> def valid?(data, options), do: if data == :fail, do: :false, else: :true
...> end
When you call validator
iex> DSV.Validator.MessageExample(:fail, first: "first constraint")
and the validation will fail, then the produced message will be:
> This is the message produced when validator is call with option `:first`
as this is the message defined for the `:first` option
If validator is called with all options like that:
iex> DSV.Validator.MessageExample(:fail, first: "first constraint", second: "second constraint", third: "third constraint")
and the validation will fail, then the produced message will be:
>This is the message produced when validator is call with all of the options `:first`, `:second`, `:third`
as this is the message defined for the options: [:first, :second, :third]
##### Custom `:message_key_mapper` implementation
There is a possibility to define a custom mapping from validation options to the message keys. By default, validation options are mapped to message keys without changes. This behavior can be changed by defining the `:message_key_mapper` option of the `Dsv.Validator` module. `:message_key_mapper` must be a function that receives options as the only argument and returns a list of keys that are defined in the `message` macros.
## Example
iex> defmodule Dsv.Validator.MessageExample do
...> use Dsv.Validator, message_key_mapper: fn options -> if length(options) == 1, do: [:one_option_one_error], else: [:many_options] end
...>
...> message {:one_option_one_error, "Validation for only one options failed."}
...> message {:many_options, "There are so many options. I do not know what has failed."}
...>
...> def valid?(data, options \\\\ [])
...> def valid?(data, options), do: if data == :fail, do: :false, else: :true
...> end
In this example, we assigned the function
fn options -> if length(options) == 1, do: [:one_option_one_error], else: [:many_options] end
to the `:message_key_mapper` option.
When you run the `validate` function on the `Dsv.Validator.MessageExample` module, and there is a failure (data parameter equal to `:fail`), the function assigned to the `:message_key_mapper` will be run with all options that you pass to the `validate` function.
Our function will check the number of options you pass to the `validate` function and will return `:one_option_one_error` in case there is exactly one option in the method call or `:many_options` otherwise.
The value returned from the function assigned to the `:message_key_mapper` will be used to choose the error message from messages defined in the `message` macro.
iex> Dsv.Validator.MessageExample.validate(:fail, one_option: false)
{:error, "Validation for only one options failed."}
iex> Dsv.Validator.MessageExample.validate(:fail, option_one: 1, option_two: 2)
{:error, "There are so many options. I do not know what has failed."}
### Complex validators
Complex vaidator will gather all error messages from all the validators used for data validation.
Complex validators allow to transform/split input data to different form/parts based on the validation options.
Transformation can be something like getting part of the input as in the `Dsv.Email` or calling function on the data like in the below `Dsv.Path` example.
Transformation need to be done by implementing `get_element/2` function if user will not provide own implementation then default one will be used.
Default implementation of `get_element/2` is just `Map.get/2` function.
To use above functionallity you need to set `:complex` option to `:true`.
## Example
defmodule Dsv.Path do
use Dsv.Validator, complex: :true
message {:basename, "Wrong basename."}
message {:dirname, "Wrong dirname."}
message {:rootname, "Wrong rootname."}
message {[:basename, :rootname, :dirname], "hohoho"}
message fn data, _options, errors -> "Path " <> data <> " doesn't meet criteria " <> (errors |> Map.values |> Enum.join(" and ")) end
def get_element(path, :basename), do: Path.basename(path)
def get_element(path, :dirname), do: Path.dirname(path)
def get_element(path, :rootname), do: Path.rootname(path)
end
Now we can use `Dsv.Path` to validate basic path info.
iex> Dsv.Path.validate("/a/b/c/d.e", basename: [equal: "d.e"], dirname: [length: [min: 2, max: 20]])
:ok
iex> Dsv.Path.validate("/a/b/c/d.e", basename: [equal: "d.e"], dirname: [length: [min: 2, max: 4]])
{:error, "Path /a/b/c/d.e doesn't meet criteria Values must be equal and Value \"/a/b/c\" has wrong length. Minimum lenght is 2, maximum length is 4"}
In the last example you can see that error message contain messages from `Dsv.Equal` and `Dsv.Length` validators combine together.
This is possible because when you define your validator with `:complex` option set to `true`, you can use in your custom error message any error from any validator used during validation of any element.
"""
defmacro __using__(opts) do
mkm = Keyword.get(opts, :message_key_mapper)
quote do
import Dsv.Validator
@before_compile unquote(__MODULE__)
defp get_element(data, name), do: Map.get(data, name)
defp get_errors(_data, _options, errors), do: errors
if unquote(mkm) != nil do
defp get_message_key(options) do
unquote(mkm).(options)
end
else
defp get_message_key(options) when is_list(options) do
if Keyword.keyword?(options), do: Keyword.keys(options), else: [nil]
end
defp get_message_key(options) do
[nil]
end
end
if Keyword.get(unquote(opts), :complex, false) do
@doc """
Validate data using multiple validators. Return :ok or map of errors related to validated fields.
`data` - data to validate
`options` - keyword list of validation options
`custom_message` - user defined message
"""
def validate(data, options \\ [])
def validate(data, options) when is_list(options) do
try do
{message, options} =
if Keyword.keyword?(options) do
Keyword.pop(options, :message)
else
{nil, options}
end
options
|> Enum.map(fn {field_name, validators} ->
{field_name, get_element(data, field_name), validators}
end)
|> Enum.map(fn {field_name, data, validators} ->
{field_name, Dsv.validate(data, validators)}
end)
|> Enum.filter(fn
{_, :ok} -> false
_ -> true
end)
|> Enum.map(fn {field_name, {:error, errors}} -> {field_name, errors} end)
|> Enum.reduce(%{}, fn elem, acc -> Map.put(acc, elem(elem, 0), elem(elem, 1)) end)
|> (fn
errors when errors == %{} ->
:ok
errors ->
case message do
nil ->
{:error, get_message(get_message_key(options), data, options, errors)}
# nil -> get_message(get_message_key(options), data, nil, options, errors)
m ->
{:error, get_custom_message(data, m, options, errors)}
end
end).()
rescue
e ->
{:error, exception_message(data, options, e, __STACKTRACE__)}
end
end
def valid?(data, options) when is_list(options),
do:
options
|> Enum.all?(fn {field_name, validators} ->
Dsv.valid?(get_element(data, field_name), validators)
end)
defoverridable valid?: 2
else
@doc """
Validate data. Return :ok or map of errors related to validated fields.
`data` - data to validate
`options` - keyword list of validation options
"""
def validate(data, options \\ [])
def validate(data, options) when is_list(options) do
try do
{message, options} =
if Keyword.keyword?(options) do
Keyword.pop(options, :message)
else
{nil, options}
end
if valid?(data, options) do
:ok
else
if message do
{:error, get_custom_message(data, message, options)}
else
{:error, get_message(get_message_key(options), data, options)}
end
end
rescue
e -> {:error, exception_message(data, options, e, __STACKTRACE__)}
end
end
end
def validate(data, options) do
try do
if valid?(data, options) do
:ok
else
{:error, get_message(get_message_key(options), data, options)}
end
rescue
e ->
{:error, exception_message(data, options, e, __STACKTRACE__)}
end
end
defoverridable validate: 2, get_element: 2
end
end
defmacro __before_compile__(_env) do
quote do
defp get_custom_message(data, custom_message, options) when is_function(custom_message, 2),
do: custom_message.(data, options)
defp get_custom_message(data, custom_message, options, errors)
when is_function(custom_message, 3),
do: custom_message.(data, options, errors)
defp get_custom_message(data, custom_message, options) when not is_nil(custom_message),
do: EEx.eval_string(custom_message, data: data, options: options)
defp get_custom_message(data, custom_message, options, errors)
when not is_nil(custom_message),
do: EEx.eval_string(custom_message, data: data, options: options, errors: errors)
defp get_message(_, _, _), do: "Unknown validation error"
defp get_message(_, _, _, _), do: "Unknown validation error"
defp exception_message(data, options, exception, stacktrace),
do: reraise(exception, stacktrace)
end
end
@doc """
The `message` macro allows defining the message that the validator will return in case of a validation failure.
There are a few ways of defining a message for a validator:
* Provide a plain string
* Provide a function that returns a string
* Provide a tuple with keys as the first element and a message string as the second element.
* Provide a tuple with keys as the first element and a function that return a string as the second element.
### Plain string
Provide message for any combination of options or no options at all.
It can be used as a default message if there is no message specified for particular options.
This version of `message` should be used as a last message definition in the module otherwise it will be always returned even if there will be message better adjusted to the options.
#### Example
iex> defmodule Dsv.IsTrue do
...> use Dsv.Validator
...>
...> message "Provided value is invalid. Accepted value is ':true'"
...>
...> def valid?(data, options \\\\ []), do: if data == :true, do: :true, else: :false
...> end
iex> Dsv.IsTrue.validate(:false)
{:error, "Provided value is invalid. Accepted value is ':true'"}
### Function that returns a string
Provide message for any combination of options or no options at all.
It can be used as a default message if there is no message specified for particular options.
This version of `message` should be used as a last message definition in the module otherwise it will be always returned even if there will be message better adjusted to the options.
The function will receive two (three in case of :complex option set to true) that can be used to create error message:
* data - input data
* options - validation options
* errors - map of errors - avaliable only when [:complex](#module-complex-validators) is set to true
#### Example
iex> defmodule Dsv.IsTrue do
...> use Dsv.Validator
...>
...> message fn _, _ -> "Provided value is invalid. Accepted value is ':true'"
...>
...> def valid?(data, options \\\\ []), do: if data == :true, do: :true, else: :false
...> end
iex> Dsv.IsTrue.validate(:false)
{:error, "Provided value is invalid. Accepted value is ':true'"}
### Tuple with keys and message
When the `message` macro is used with the tuple, the first element decides which message should be used on validation failure.
The second element of the tuple is the message that will be returned on failure.
By default message is connected with validation options.
It is possible to change this behaviour by passing `message_key_mapper` option to the `Dsv.Validator` module.
#### Example
defmodule Dsv.IsTrue do
use Dsv.Validator, message_key_mapper: fn
:g -> [:general]
:a -> [:additional_info]
_ -> []
end
message {:general, "Provided value is invalid. Accepted value is ':true'"}
message {:additional_info, "Provided value '<%= inspect data %>' is invalid. The only accepted value is ':true'"}
message "This will be used in case options doesn't match any previous value"
def valid?(data, options \\\\ []), do: if data == :true, do: :true, else: :false
end
iex> Dsv.IsTrue.validate(:false, :g)
{:error, "Provided value is invalid. Accepted value is ':true'"}
iex> Dsv.IsTrue.validate(:false, :a)
{:error, "Provided value 'false' is invalid. The only accepted value is ':true'"}
iex> Dsv.IsTrue.validate(:false, :anything_else)
{:error, "This will be used in case options doesn't match any previous value"}
iex> Dsv.IsTrue.validate(:false)
{:error, "This will be used in case options doesn't match any previous value"}
To associate message with multiple options, provide options in the list as the first argument of the tuple pass to `message` macro.
Message associated with multiple options will be returned no matter in what order user will use those options in the validate function.
#### Example - validator with `:min` and `:max` options.
In the case of a validator that accepts two options (`:min` and `:max` in this case) we can create messages for each option combination (`:min`, `:max` and `[:min, :max]` - in this case, it doesn't matter in what order we provide `[:min, :max]` or `[:max, :min]` options to the message macro as long as validator accepts them in any order).
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value is too short"}
...> message {:max, "Value is too long"}
...> message {[:min, :max], "Value is too short or too long"}
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> def valid?(data, [{:max, max_length}, {:min, min_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
iex> Dsv.StringLength.validate("Test", min: 10)
{:error, "Value is too short"}
iex> Dsv.StringLength.validate("Test", max: 3)
{:error, "Value is too long"}
iex> Dsv.StringLength.validate("Test", min: 3, max: 4)
{:error, "Value is too short or too long"}
iex> Dsv.StringLength.validate("Test", max: 4, min: 3)
{:error, "Value is too short or too long"}
### Order of the message definition
Order of message definition is important. In case of overlapping definitions, the first message that matches will be used.
### Default message
If there is no definition of the message, the default value ("Unknown validation error") will be returned in case of any validation error.
### Additional information in the error message
Defining messages with data and options information. Besides simple messages like the above, we can create messages that will contain data under validation as well as options used during validation.
There are two ways of creating such message:
* EEx (Embedded Elixir) - the EEx string can use `data`, `options`, and `errors` values.
* Function
* with two arguments:
* data
* options
* with three arguments:
* data
* options
* errors
In the case of EEx as well as function data are the data that are under validation, options are the options provided to the validator and errors are the errors from sub validator in the case of `:complex` validator.
defmodule Dsv.StringLength do
use Dsv.Validator
message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
message {[:min, :max], fn data, options -> "Value '\#\{data\}' is shorter than expected \#\{options[:min]\} or longer than expected \#\{options[:max]\} characters." end}
def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
end
iex> Dsv.StringLength.validate("test", min: 6)
{:error, "Value 'test' is shorter than expected 6 characters."}
iex> Dsv.StringLength.validate("test", max: 2)
{:error, "Value 'test' is longer than expected 2 characters."}
iex> Dsv.StringLength.validate("test", min: 10, max: 100)
{:error, "Value 'test' is shorter than expected 10 or longer than expected 100 characters."}
In case of validation failure, we get a tuple with `:error` as the first element and the error message as the second element.
"""
defmacro message({keys, message}) do
for arg_list <- Permutations.permutations(keys) do
quote do
defp get_message(unquote(arg_list), data, options) do
generate_message(unquote(message), data, options)
end
defp get_message(unquote(arg_list), data, options, errors) do
generate_message(unquote(message), data, options, errors)
end
end
end
end
defmacro message(message) do
quote do
defp get_message(_, data, options) do
generate_message(unquote(message), data, options)
end
defp get_message(_, data, options, errors) do
generate_message(unquote(message), data, options, errors)
end
end
end
@doc false
def generate_message(message, data, options) when is_function(message, 2),
do: message.(data, options)
@doc false
def generate_message(message, data, options) when is_function(message, 3),
do: message.(data, options, %{})
def generate_message(message, data, options) when is_bitstring(message),
do: EEx.eval_string(message, data: data, options: options)
@doc false
def generate_message(message, data, options, errors) when is_function(message, 3),
do: message.(data, options, errors)
def generate_message(message, data, options, errors) when is_bitstring(message),
do: EEx.eval_string(message, data: data, options: options, errors: errors)
@doc """
The `e_message` macro allow to define the message that the validator will return in case of an exception. There are two possible ways to define a message:
* Provide a plain string or string in a EEx format
* Provide a function that will receive three arguments:
* data - data for validation
* options - validation options
* exception - exception returned by the validator
## Example
### Plain string
defmodule Dsv.StringLength do
use Dsv.Validator
message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
e_message "Unexpected error occured during validation. The data or options you provided are incorrect."
def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
end
iex> Dsv.StringLength.validate(4443432, min: 6)
{:error, "Unexpected error occured during validation. The data or options you provided are incorrect."}
### String in EEx format
The message can contain information from fields such as data, options, and exceptions, where data is the data passed to the validator, options are the options passed to the validator, and an exception is an exception thrown during validation.
defmodule Dsv.StringLength do
use Dsv.Validator
message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
e_message "Unexpected error <%= inspect exception %> occured during validation with data: [<%= inspect data %>] and options: [<%= inspect options %>]."
def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
end
iex> Dsv.StringLength.validate(4443432, min: 6)
{:error, "Unexpected error %FunctionClauseError{args: nil, arity: 1, clauses: nil, function: :length, kind: nil, module: String} occured during validation with data: [4443432] and options: [[min: 6]]."}
### Function
The function will receive three arguments that we can use to create an error message:
* data - data for validation
* options - validation options
* exception - exception returned by the validator
#### Example
iex> defmodule Dsv.StringLength do
...> use Dsv.Validator
...>
...> message {:min, "Value '<%= data %>' is shorter than expected <%= options[:min] %> characters."}
...> message {:max, "Value '<%= data %>' is longer than expected <%= options[:max] %> characters."}
...> message {[:min, :max], "Value '<%= data %>' is shorter than expected <%= options[:min] %> or longer than expected <%= options[:max] %> characters."}
...>
...> e_message fn data, options, exception -> "Unexpected error occured during '\#\{data\} validation with options \#\{options\}. The data or options you provided are incorrect. Exception: \#\{exception\}." end
...>
...> def valid?(data, [{:min, min_length}]), do: String.length(data) > min_length
...> def valid?(data, [{:max, max_length}]), do: String.length(data) < max_length
...> def valid?(data, [{:min, min_length}, {:max, max_length}]), do: valid?(data, min: min_length) and valid?(data, max: max_length)
...> end
iex> Dsv.StringLength.validate(4443432, min: 6)
{:error, "Unexpected error occured during '4443432 validation with options [min: 6]. The data or options you provided are incorrect. Exception: %FunctionClauseError{args: nil, arity: 1, clauses: nil, function: :length, kind: nil, module: String}."}
"""
defmacro e_message({:fn, _, _} = message_supplier) do
quote do
defp exception_message(data, options, exception, _),
do: unquote(message_supplier).(data, options, exception)
end
end
defmacro e_message({:&, _, _} = message_supplier) do
quote do
defp exception_message(data, options, exception, _),
do: unquote(message_supplier).(data, options, exception)
end
end
defmacro e_message(message) do
quote do
defp exception_message(data, options, exception, _),
do: EEx.eval_string(unquote(message), data: data, options: options, exception: exception)
end
end
@doc """
This is function that has default implementation and don't need to be implemented.
When you will leave default implementation it will use `valid?` function to check if input data are valid and return `:ok` or `{:error, error}` result.
The error value in the error result will contain default message or the message defined in the validator by the `Dsv.Validator.message/1` macro.
"""
@type input_data() :: any()
@callback validate(input_data(), any()) :: :ok | {:error, String.t()}
@doc """
This is a function that need to be implemented in a validator that use `Dsv.Validator` module.
This is basic function to validated input data.
"""
@callback valid?(input_data(), any()) :: boolean()
end