View Source Dsv Validator Documentation

Welcome to the guide on validating data with the Dsv Validator module. Dsv provides multiple ways to validate data, catering to different user preferences. This document outlines the various methods available for validating data.

Overview of Validation Approaches

Dsv offers different ways to validate data:

  1. Basic Validation: Perform validation on a single value using a keywrod list validators description.
  2. Pipeline Validation: Define validation rules for each field in a structured, readable manner.
  3. Mirror Data Structure Validation: Validate complex data structures by mirroring the structure.
  4. List of Paths and Validators: Use a list of paths and associated validators for nested data structures.

Basic Validation

Basic validation allows you to validate individual values quickly using the Dsv.validate/2 function. Pass the value to validate as the first argument and specify the validator in the second argument.

Single value one validator

To validate simple data like string, number etc. use Dsv.validate/2 function.

Example single value validation with single validator:

iex> Dsv.validate("Test name", length: [min: 3])
:ok

This code will check if "Test name" value is at least of length of 3. As the validator passed, result is :ok

Single value multiple validators

You can combine multiple validators, and validation passes only if all of them succeed. When the value is valid, Dsv.validate/2 returns :ok. If invalid, it returns a tuple with a list of errors.

Example - basic validation, multiple validators

Check if the value has a minimum length of 3 and starts with "Test".

iex> Dsv.validate("Test name", length: [min: 3], format: ~r/^Test.*/)
:ok

Example - basic validation, multiple validators, failed validation

Check if the value has a minimum length of 3, maximum length of 4 and starts with "Test".

iex> Dsv.validate("Wrong name", length: [min: 3, max: 4], format: ~r/^Test.*/)
{
  :error,
  [
      "Value \"Wrong name\" has wrong length. Minimum lenght is 3, maximum length is 4",
      "Value Wrong name does not match pattern ^Test.*"
  ]
}

Pipeline validations

Pipeline validation allows for structured and readable approach to complex data structure validation. This approach consists of three steps: creating validation, adding validators, and validating data.

Create validation

To use pipeline validations, Dsv needs to create a data structure that gathers all the information about the validation rules. Dsv.validation/1 allows you to create this structure. To do so, call this function with the data to validate.

create validation example

    validation = Dsv.validation(%{
        "first" => "This is the first element to validate", 
        "second" => %{
            "third" => 42, "fourth" => [-1, 0, 1]
        }
    })

This is the first step. The result of this step can the be pipelined into the Dsv.add_validator/3 function.

Add validators

After creating the validation, the next step is to add all the desirable validators. For each field that will be validated, you should call Dsv.add_validator/3. This function will add a list of validators to the field specified in the path. The path is in the form of a list, where each element of the list represents a part of the path to the validated element.

Add validators example

    validation = validation
    |> Dsv.add_validator(["first"], length: [min: 1])
    |> Dsv.add_validator(["second", "fourth"], length: [max: 20], position: ["1": [equal: 0]])

Validate data

The final step in the pipeline is to initiate the validation process by calling the Dsv.validate function.

validate data example

    validation |> Dsv.validate

Pipeline validation - full example

    %{"first" => "This is the first element to validate", "second" => %{"third" => 42, "fourth" => [-1, 0, 1]}}
    |> Dsv.add_validator(["first"], length: [min: 1])
    |> Dsv.add_validator(["second", "fourth"], length: [max: 20], position: ["1": [equal: 0]])
    |> Dsv.validate

Pipeline validation - full example - data to validate are provided at the end

Dsv.validation
    |> Dsv.add_validator(["first"], length: [min: 1])
    |> Dsv.add_validator(["second", "fourth"], length: [max: 20], position: ["1": [equal: 0]])
    |> Dsv.validate(%{"first" => "This is the first element to validate", "second" => %{"third" => 42, "fourth" => [-1, 0, 1]}})

This approach allow to use the same validator multiple times with different data.

simple_validator = Dsv.validation
    |> Dsv.add_validator(["first"], length: [min: 1])
    |> Dsv.add_validator(["second", "fourth"], length: [max: 20], position: ["1": [equal: 0]])

simple_validator
    |> Dsv.validate(%{"first" => "This is the first element to validate", "second" => %{"third" => 42, "fourth" => [-1, 0, 1]}})

simple_validator
    |> Dsv.validate(%{"first" => "Different data to validate ", "second" => %{"third" => 2, "fourth" => [1, 0, 1]}})

simple_validator
|> Dsv.validate(%{"first" => "Different data to validate ", "second" => %{"third" => 2, "fourth" => [1, 1, 1]}})
{:error, %{"second" => %{"fourth" => [%{"1": ["Values must be equal"]}]}}}

Two fields from validated data in one validator

Sometimes there is a need to use two different fields from input data in a single validator. A good example is when a user sends a new password and a password confirmation. In such cases, the validator needs to check if both values are equal. To accomplish this, the Dsv.bind/3 function is designed. It is intended to accept path to the binded value and the name for the binded value. Next whenewer binded value is needed Dsv.binded/1 function need to be used with the name of the binded value.

Two fields from validated data in one validator - example

    %{user: %{password: "test", password_confirmation: "test_confirmation"}}
    |> Dsv.validation
    |> Dsv.bind([:user, :password_confirmation], :pass_confirmation)
    |> Dsv.add_validator([:user, :password], length: [min: 8, max: 40])
    |> Dsv.add_validator([:user, :password], equal: Dsv.binded(:pass_confirmation))
    |> Dsv.validate

This will give the same result as:

    %{user: %{password: "test", password_confirmation: "test_confirmation"}}
    |> Dsv.validation
    |> Dsv.add_validator([:user, :password], length: [min: 8, max: 40])
    |> Dsv.add_validator([:user, :password], equal: "test_confirmation")
    |> Dsv.validate

Two fields from validate data in one validator - number example

    %{min_members: 10, max_members: 100, actual_members: 30}
    |> Dsv.validation
    |> Dsv.bind([:min_members], :min_members)
    |> Dsv.bind([:max_members], :max_members)
    |> Dsv.add_validator([:actual_members], number: [gte: Dsv.binded(:min_members), lte: Dsv.binded(:max_members))
    |> Dsv.validate

This can be translated to:

    %{min_members: 10, max_members: 100, actual_members: 30}
    |> Dsv.validation
    |> Dsv.add_validator([:actual_members], number: [gte: 10, lte: 100])
    |> Dsv.validate

Struct example

It is possible to use pipelines to validate structs.

Lets define Address struct and the Person struct which will contain address.

defmodule Address do
  defstruct [:street, :city, :country, :postal_code]
end

defmodule Person do
  defstruct [:name, :surname, :age, :address]
end

Now we can validate those data

%Person{name: "Martin", surname: "Smith", age: 43, address: %Address{street: "St Marks Ave", city: "New York", country: "USA", postal_code: "11213"}}
|> Dsv.validation
|> Dsv.add_validator([:name], length: [min: 2, max: 30])
|> Dsv.add_validator([:surname], length: [min: 2, max: 30])
|> Dsv.add_validator([:age], number: [gt: 0, lt: 140])
|> Dsv.add_validator([:address, :street], length: [min: 1, max: 100])
|> Dsv.add_validator([:address, :country], in: ["USA", "Canada", "Mexico"])
|> Dsv.validate   

Mirror data structure

This approach involves validating complex data structures by mirroring the structure in the validation rules, to do that use Dsv.validate/2 function.

Basic success example

Data to validate

 user_info = %{
      "name" => "Martin",
      "age" => 45,
      "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
      "company_data" => %{
        "name" => "company name",
        "address" => %{
          "city" => "New York",
          "zip code" => "10001"
        }
      },
      "skills" => %{
        :programming => 100,
        :cooking => 60
      }
 }

Validate user info data

Dsv.validate(user_info, %{
   "name" => [length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
   "age" => [number: [gte: 21, lte: 120]],
   "company_data" => %{
     "name" => [length: [min: 2]],
     "address" => %{
       "city" => [length: [min: 2]],
       "zip code" => [format: ~r/[0-9]{1,5}/]
     }
   }
})

In this example input data meets validation createria, so the result of the validation will be :ok.

Basic failure example

Map with data to validate.

 user_info = %{
    "name" => "M",
    "age" => 145,
    "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
    "company_data" => %{
      "name" => "",
      "address" => %{
        "city" => "New York",
        "zip code" => "a10001"
      }
    },
    "skills" => %{
      :programming => 100,
      :cooking => 60
    }
 }

Validate data

Dsv.validate(user_info, %{
   "name" => [length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
   "age" => [number: [gte: 21, lte: 120]],
   "company_data" => %{
     "name" => [length: [min: 2]],
     "address" => %{
       "city" => [length: [min: 2]],
       "zip code" => [format: ~r/[0-9]{1,5}/]
     }
   }
})

In this case validator will return a tuple with a map of errors:

{:error,
 %{
   "age" => ["Value 145 must be lower than or equal: 120 and greater than or equal: 21"],
   "company_data" => %{
     "name" => ["Value \"\" is to short. Minimum lenght is 2"]
   },
   "name" => [
      "Value \"M\" has wrong length. Minimum lenght is 2, maximum length is 50",
      "Value M does not match pattern ^[A-Z][a-z]+"
    ]
 }}

Use input data as validator argument

Sometimes there is a need to use two different fields from input data in a single validator. In this example, we want to compare the levels of two different skills. To achieve this, the Dsv.validate/3 function is designed. This function operates similarly to Dsv.validate/2 but with the addition of a map with binded values. It is meant to accept a map where key is the binded value name and value is a path to the binded value. To use binded value in validators Dsv.binded/1 function need to be used with binded value name as a parameter.

Success example

   user_info = %{
      "name" => "Martin",
      "age" => 45,
      "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
      "company_data" => %{
        "name" => "Company name",
        "address" => %{
          "city" => "New York",
          "zip code" => "10001"
        }
      },
      "skills" => %{
        :programming => 100,
        :cooking => 60
      }
   }

  binded_values = %{
    cooking_skills: ["skills", :cooking]
  }

  Dsv.validate(user_info, %{
    "name" => [length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
    "age" => [number: [gte: 21, lte: 120]],
    "skills" => %{
      :programming => [number: [gt: Dsv.binded(:cooking_skills)]]
    },
    "company_data" => %{
      "name" => [length: [min: 2]],
      "address" => %{
        "city" => [length: [min: 2]],
        "zip code" => [format: ~r/[0-9]{1,5}/]
      }
    }
  }, binded_values: binded_values)
  :ok

Failure example

   user_info = %{
      "name" => "Martin",
      "age" => 45,
      "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
      "company_data" => %{
        "name" => "Company name",
        "address" => %{
          "city" => "New York",
          "zip code" => "10001"
        }
      },
      "skills" => %{
        :programming => 100,
        :cooking => 60
      }
   }

  binded_values = %{
    cooking_skills: ["skills", :cooking]
  }

  Dsv.validate(user_info, %{
    "name" => [length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
    "age" => [number: [gte: 21, lte: 120]],
    "skills" => %{
      :programming => [equal: Dsv.binded(:cooking_skills)]
    },
    "company_data" => %{
      "name" => [length: [min: 2]],
      "address" => %{
        "city" => [length: [min: 2]],
        "zip code" => [format: ~r/[0-9]{1,5}/]
      }
    }
  }, binded_values: binded_values)
  {:error, %{"skills" => %{programming: ["Values must be equal"]}}}

Struct example

As in other approches it is possible to use map with validators to validate structs.

Lets define Address struct and the Person struct which will contain address.

defmodule Address do
  defstruct [:street, :city, :country, :postal_code]
end

defmodule Person do
  defstruct [:name, :surname, :age, :address]
end

Now we can validate those data

person = %Person{name: "Martin", surname: "Smith", age: 43, address: %Address{street: "St Marks Ave", city: "New York", country: "USA", postal_code: "11213"}}
Dsv.validate(person, %{
  name: [length: [min: 2, max: 30]],
  surname: [length: [min: 2, max: 30]],
  age: [number: [gt: 0, lt: 140]],
  address: %{
    street: [length: [min: 1, max: 100]],
    country: [in: ["USA", "Canada", "Mexico"]]
  }
})

List of paths and validators

This approach involves using a list of keyword lists, where the path key is used to define the path to the validated field, and other keys represent the names of the validators.

List of paths and validators - example

user_info = %{
  "name" => "Martin",
  "age" => 45,
  "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
  "company_data" => %{
    "name" => "company name",
    "address" => %{
      "city" => "New York",
      "zip code" => "10001"
    }
  },
  "skills" => %{
    :programming => 100,
    :cooking => 60
  }
}

Dsv.validate(user_info, paths([
  [path: ["name"], length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
  [path: ["age"], number: [gte: 21, lte: 120]],
  [path: ["company_data", "name"], length: [min: 2]],
  [path: ["company_data", "address", "city"], length: [min: 2]],
  [path: ["company_data", "address", "zip code"], format: ~r/[0-9]{1,5}/]
]))

List of paths - error example

user_info = %{
  "name" => "M",
  "age" => 145,
  "programming_languages" => ["PHP", "Python", "Java", "Go", "Elixir", "Kotlin"],
  "company_data" => %{
    "name" => "",
    "address" => %{
      "city" => "New York",
      "zip code" => "a10001"
    }
  },
  "skills" => %{
    :programming => 100,
    :cooking => 60
  }
}


Dsv.validate(user_info, Dsv.paths([
  [path: ["name"], length: [min: 2, max: 50], format: ~r/^[A-Z][a-z]+/],
  [path: ["age"], number: [gte: 21, lte: 120]],
  [path: ["company_data", "name"], length: [min: 2]],
  [path: ["company_data", "address", "city"], length: [min: 2]],
  [path: ["company_data", "address", "zip code"], format: ~r/[0-9]{1,5}/]
]))
{:error,
 %{
   "age" => ["Value 145 must be lower than or equal: 120 and greater than or equal: 21"],
   "company_data" => %{
     "name" => ["Value \"\" is to short. Minimum lenght is 2"]
   },
   "name" => [
      "Value \"M\" has wrong length. Minimum lenght is 2, maximum length is 50",
      "Value M does not match pattern ^[A-Z][a-z]+"
   ]
 }}

Struct example

As in other approches it is possible to use list of paths with validators to validate structs.

Lets define Address struct and the Person struct which will contain address.

defmodule Address do
  defstruct [:street, :city, :country, :postal_code]
end

defmodule Person do
  defstruct [:name, :surname, :age, :address]
end

Now we can validate those data

person = %Person{name: "Martin", surname: "Smith", age: 43, address: %Address{street: "St Marks Ave", city: "New York", country: "USA", postal_code: "11213"}}
Dsv.validate(person, Dsv.paths([
  [path: [:name], length: [min: 2, max: 30]],
  [path: [:surname], length: [min: 2, max: 30]],
  [path: [:age], number: [gt: 0, lt: 140]],
  [path: [:address, :street], length: [min: 1, max: 100]],
  [path: [:address, :country],  in: ["USA", "Canada", "Mexico"]]
]))