View Source Dsv.Validator behaviour (Dsv v0.1.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.

Link to this callback

validate(input_data, any)

View Source
@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

Link to this macro

e_message(message_supplier)

View Source (macro)

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}."}
Link to this macro

message(message)

View Source (macro)

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, 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.