lib/validator.ex

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

      defp resolve_options({:binded, _} = options, binded_values),
        do: value(options, binded_values)

      defp resolve_options(options, binded_values) when is_list(options),
        do:
          options
          |> Enum.map(fn
            {k, v} -> {k, value(v, binded_values)}
            elem -> resolve_options(elem, binded_values)
          end)

      defp resolve_options(options, _binded_values), do: options

      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
          validate(
            data,
            options,
            nil,
            &get_message/4,
            &get_message_key/1,
            &get_custom_message/4,
            &get_element/2,
            &exception_message/4
          )
        end

        def validate(data, options, binded_values) when is_list(options) do
          validate(
            data,
            options,
            binded_values,
            &get_message/4,
            &get_message_key/1,
            &get_custom_message/4,
            &get_element/2,
            &exception_message/4
          )
        end

        def valid?(data, options \\ [])

        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)

        def valid?(data, options, binded_values),
          do: valid?(data, resolve_options(options, binded_values))

        defoverridable valid?: 2, valid?: 3, validate: 2, validate: 3
      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
          validate(
            data,
            options,
            &get_custom_message/3,
            &get_message/3,
            &get_message_key/1,
            &exception_message/4,
            &valid?/2
          )
        end

        def validate(data, options, binded_values)
            when is_list(options) and is_map(binded_values) do
          validate(
            data,
            options,
            binded_values,
            &get_custom_message/3,
            &get_message/3,
            &get_message_key/1,
            &exception_message/4,
            &valid?/3,
            &resolve_options/2
          )
        end

        def valid?(data, options, binded_values),
          do: valid?(data, resolve_options(options, binded_values))

        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

        def validate(data, options, binded_values) do
          try do
            if valid?(data, options, binded_values) do
              :ok
            else
              resolved_options = resolve_options(options, binded_values)

              {:error,
               get_message(
                 get_message_key(resolved_options),
                 data,
                 resolved_options
               )}
            end
          rescue
            e ->
              {:error,
               exception_message(data, resolve_options(options, binded_values), e, __STACKTRACE__)}
          end
        end

        defoverridable validate: 2, validate: 3, valid?: 3
      end

      @doc false
      def value({:binded, name}, binded_values), do: Map.get(binded_values, name, name)
      def value(value, binded_values), do: value

      defoverridable 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 false
  def validate(
        data,
        options,
        binded_values,
        get_message_function,
        get_message_key_function,
        get_custom_message_function,
        get_element_function,
        exception_message
      ) do
    try do
      {message, options} =
        if Keyword.keyword?(options), do: Keyword.pop(options, :message), else: {nil, options}

      validators = Enum.into(options, %{})

      data_to_validate =
        Keyword.keys(options)
        |> Enum.map(&{&1, get_element_function.(data, &1)})
        |> Enum.into(%{})

      result =
        case binded_values do
          nil ->
            ValidatorPipeline.run_validation(data_to_validate, validators)

          _ ->
            ValidatorPipeline.run_validation(data_to_validate, validators,
              binded_values: binded_values
            )
        end

      case result do
        {:error, errors} ->
          case message do
            nil ->
              {:error,
               get_message_function.(get_message_key_function.(options), data, options, errors)}

            _ ->
              {:error, get_custom_message_function.(data, message, options, errors)}
          end

        _ ->
          result
      end
    rescue
      e -> {:error, exception_message.(data, options, e, __STACKTRACE__)}
    end
  end

  @doc false
  def validate(
        data,
        options,
        get_custom_message,
        get_message,
        get_message_key,
        exception_message,
        valid?
      )
      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

  @doc false
  def validate(
        data,
        options,
        binded_values,
        get_custom_message,
        get_message,
        get_message_key,
        exception_message,
        valid?,
        resolve_options
      )
      when is_list(options) and is_map(binded_values) do
    try do
      {message, options} =
        if Keyword.keyword?(options) do
          Keyword.pop(options, :message)
        else
          {nil, options}
        end

      if valid?.(data, options, binded_values) do
        :ok
      else
        resolved_options = resolve_options.(options, binded_values)

        if message do
          {:error, get_custom_message.(data, message, resolved_options)}
        else
          {:error,
           get_message.(
             get_message_key.(resolved_options),
             data,
             resolved_options
           )}
        end
      end
    rescue
      e ->
        {:error,
         exception_message.(data, resolve_options.(options, binded_values), e, __STACKTRACE__)}
    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