View Source Dsv.Validator behaviour (Dsv v0.2.1)
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.
Summary
Callbacks
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.
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.
Functions
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
The message
macro allows defining the message that the validator will return in case of a validation failure.
Types
@type input_data() :: any()
Callbacks
@callback valid?(input_data(), any()) :: boolean()
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 validate(input_data(), any()) :: :ok | {:error, String.t()}
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.
Functions
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}."}
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 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
, anderrors
values. - Function
- with two arguments:
- data
- options
- with three arguments:
- data
- options
- errors
- with two arguments:
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.