Validator meets the following requirements:
- Rules are declarative and language independent
- Any number of rules for each field
- Validator should return together errors for all fields
- Exclude all fields that do not have validation rules described
- Possibility to validate complex hierarchical structures
- Easy to describe and understand validation
- Returns understandable error codes (neither error messages nor numeric codes)
- Easy to implement own rules (usually you will have several in every project)
- Rules should be able to change results output ("trim", "nested_object", for example)
- Multipurpose (user input validation, configs validation, contracts programming etc)
- Unicode support
- JavaScript - https://github.com/koorchik/js-validator-livr (Available for node.js(npm) and browsers)
- Perl - https://github.com/koorchik/Validator-LIVR (Available on CPAN)
- PHP - https://github.com/WebbyLab/php-validator-livr (https://packagist.org/packages/validator/livr)
- Python (https://github.com/asholok/python-validator-livr) (https://pypi.python.org/pypi/LIVR)
Simple registration data (demo)
{
name: 'required',
email: ['required', 'email'],
gender: { one_of: ['male', 'female'] },
phone: {max_length: 10},
password: ['required', {min_length: 10} ]
password2: { equal_to_field: 'password' }
}
- 'required' is a shorter form of { 'required': [] }
- {max_length: 10} is a shorter form of {max_length: [10]} See "How it works" section
Simple validation of nested object (demo)
{
name: 'required',
phone: {max_length: 10},
address: { 'nested_object': {
city: 'required',
zip: ['required', 'positive_integer']
}}
}
- {nested_object: {}} is a shorter form of {nested_object: [{}]} See "How it works" section
Simple list validation (demo)
{
order_id: ['required', 'positive_integer'],
product_ids: { 'list_of': [ 'required', 'positive_integer' ] }
}
Validating list of objects (demo)
{
order_id: ['required', 'positive_integer'],
products: [ 'not_empty_list', { 'list_of_objects': {
product_id: ['required','positive_integer'],
quantity: ['required', 'positive_integer']
}}]
}
Validating list of different objects (demo)
{
order_id: ['required', 'positive_integer'],
products: ['required', { 'list_of_different_objects': [
'product_type', {
material: {
product_type: 'required',
material_id: ['required', 'positive_integer'],
quantity: ['required', {'min_number': 1} ],
warehouse_id: 'positive_integer'
},
service: {
product_type: 'required',
name: ['required', {'max_length': 10} ]
}
}
]}]
}
Output modification
{
email: ['trim', 'required', 'email', 'to_lc']
}
- "trim" removes leading and trailing spaces. (skips empty values and object references)
- "to_lc" transforms string to lower case. (skips empty values and object references)
You can create pipeline with any filters you like.
You should define a structure: FIELD_NAME: VALIDATION_RULE
- FIELD_NAME is a name of field to validate
- VALIDATION_RULE is a name of function to be called. It can be function that builds validator or maybe just a validation function itself. Some arguments cab be passed to the function - "{ VALIDATION_RULE: ARGUMENTS }". You may pass an array of validation rules. If you want to pass several arguments you should use array.
Examples:
'required' or {'required': [] } becomes:
required();
{ 'max_length': 5 } or { 'max_length': [5] } becomes:
max_length(5);
{'length_between': [1,10] } becomes:
length_between(1,10);
{'one_of': [['Kiev','Moscow']] } (this is old syntax) becomes:
one_of(['Kiev', 'Moscow']);
{'one_of': ['Kiev','Moscow'] } (supported from v0.4) becomes:
one_of('Kiev', 'Moscow');
{'my_own_rule': [1, [2, 3], 'bla'] } becomes:
my_own_rule(1, [2, 3], 'bla');
Validator receives value to validate and returns an error message(in case of failed validation) or empty string(in case of success). Thats all.
So, the idea is that there is a tiny core which can be easly extended with new rules.
Be aware that all standard rules just skip checking empty values. So, empty string will pass next validation - "first_name: { min_length: [10] }". We have special rules "required" and "not_empty" to check that value is present. This allows us to use the same rules for not required fields.
first_name: { min_length: [10] } # name is optional. We will check length only if "first_name" was passed
first_name: [ 'required', { min_length: [10] } ] # check that the name is present and validate length
Standard rules that should be supported by every implementation:
- Base Rules
- required
- not_empty
- not_empty_list
- String Rules
- one_of
- max_length
- min_length
- length_between
- length_equal
- like
- Numeric Rules
- integer
- positive_integer
- decimal
- positive_decimal
- max_number
- min_number
- number_between
- Special Rules
- url
- iso_date
- equal_to_field
- Helper Rules
- nested_object
- list_of
- list_of_objects
- list_of_different_objects
- Filter rules
- trim
- to_lc
- to_uc
- remove
- leave_only
Error code: 'REQUIRED'
Example:
{first_name: 'required'}
Error code: 'CANNOT_BE_EMPTY'
Example:
{first_name: 'not_empty'}
Checks that list contains at least one element
Error code: 'CANNOT_BE_EMPTY' (If the value is not present or list is empty). If the value is present but it is not a list the error code will be 'WRONG_FORMAT'
Example:
{products_ids: 'not_empty_list'}
Error code: 'NOT_ALLOWED_VALUE'
Example:
// new syntax (introduced in v0.4)
{ first_name: {'one_of': ['Anton', 'Igor']} }
// old syntax
{ first_name: {'one_of': [['Anton', 'Igor']]} }
Error code: 'TOO_LONG'
Example:
{first_name: {max_length: 10}
Error code: 'TOO_SHORT'
Example:
{first_name: {min_length: 2}
Error code: 'TOO_LONG' or 'TOO_SHORT'
Example:
{first_name: {length_between: [2, 10] }
Error code: 'TOO_LONG' or 'TOO_SHORT'
Example:
{first_name: {length_equal: 7}
Error code: 'WRONG_FORMAT'
Example:
{first_name: {like: '^\w+?$'}
{first_name: {like: ['^\w+?$', 'i']} // with flags
Only 'i' flag is currently required by specification.
Be aware that regular expressions can be language dependent. Try to use most common syntax.
Error code: 'NOT_INTEGER'
Example:
{age: 'integer'}
Error code: 'NOT_POSITIVE_INTEGER'
Example:
{age: 'positive_integer'}
Error code: 'NOT_DECIMAL'
Example:
{price: 'decimal'}
Error code: 'NOT_POSITIVE_DECIMAL'
Example:
{price: 'positive_decimal'}
Error code: 'TOO_HIGH'
Example:
{age: { 'max_number': 95 } }
Error code: 'TOO_LOW'
Example:
{age: { 'min_number': 18 } }
Error code: 'TOO_HIGH' or 'TOO_LOW'
Example:
{age: { 'number_between': [18, 95] } }
Error code: 'WRONG_EMAIL'
Example:
{login: 'email'}
Allows you to validate url. Allows 'HTTP' and 'HTTPS' protocols. Protocol is required.
Error code: 'WRONG_URL'
Example:
{url: 'url'}
Check whether a value is an ISO 8601 date (without time). Allowed date example - "2014-08-14"
Error code: 'WRONG_DATE'
Example:
{date: 'iso_date'}
Error code: 'FIELDS_NOT_EQUAL'
Example:
{password2: {'equal_to_field': 'password' }}
Allows you to describe validation rules for a nested object.
Error code: depends on nested validators. If nested object is not a hash should return "FORMAT_ERROR"
Example:
address: { 'nested_object': {
city: 'required',
zip: ['required', 'positive_integer']
} }
Allows you to describe validation rules for a list. Validation rules will be applyed for each array element.
Error code: depends on nested validators
Example:
// new syntax (introduced in v0.4)
{ product_ids: { 'list_of': 'positive_integer' }}
{ product_ids: { 'list_of': ['required', 'positive_integer'] }} // new syntax
// old syntax
{ product_ids: { 'list_of': [[ 'positive_integer' ]] }}
{ product_ids: { 'list_of': [[ 'required', 'positive_integer' ]] }}
Allows you to describe validation rules for list of objects. Validation rules will be applyed for each array element.
Error code: depends on nested validators. Or "FORMAT_ERROR" in case of receiving data not suitable for validation.
Example:
products: ['required', { 'list_of_objects': [{
product_id: ['required','positive_integer'],
quantity: ['required', 'positive_integer']
}]}]
Allows you to describe validation rules for list of different objects. Validation rules will be applied for each array element.
Error code: depends on nested validators. Or "FORMAT_ERROR" in case of receiving data not suitable for validation.
Example:
products: ['required', { 'list_of_different_objects': [
product_type, {
material: {
product_type: 'required',
material_id: ['required', 'positive_integer'],
quantity: ['required', {'min_number': 1} ],
warehouse_id: 'positive_integer'
},
service: {
product_type: 'required',
name: ['required', {'max_length': 10} ]
}
}
]}]
In this example validator will look on "product_type" in each object and depending on it will use corresponding set of rules
Additional rules for data modification. They do not return errors just skips values that are not appropriate.
Removes leading and trailing spaces. Skips object references.
Example:
{email: 'trim'}
Converts string to lower case. Skips object references.
Example:
{email: 'to_lc'}
Converts string to upper case. Skips object references.
Example:
{currency_code: 'to_uc'}
Removes characters from string
Example:
{ text: { remove: '0123456789' } } // Remove all numbers from text
Removes characters from string
Example:
{ text: { leave_only: '0123456789' } } // Leaves only numbers in text
With rules aliasing you can create custom rules easely and assign own error codes in case own need. Moreover, aliased rules will work across all implementations as they are just data structures.
Alias struture:
{
name: ALIAS_NAME,
rules: ALIAS_RULES,
error: ERROR_CODE (optional)
}
If ERROR_CODE is empty than validator will return subrules errors.
Example:
{
name: 'valid_address',
rules: { nested_object: {
country: 'required',
city: 'required',
zip: 'positive_integer'
}}
}
{
name: 'adult_age'
rules: [ 'positive_integer', { min_number: 18 } ],
error: 'WRONG_AGE'
}
Moreover, you can use aliased rules in aliased rules.
Let's assume that you have next data structure:
{
name: 'Viktor',
age: 30,
address: {
country: 'Ukraine',
city: 'Kiev',
zip: '11111'
}
}
And you have next validation rules for it:
{
name: 'required',
age: ['required', 'positive_integer', { min_number: 18 } ],
address: ['required', {nested_object: {
country: 'required',
city: 'required',
zip: 'positive_integer'
}}]
}
You use 'address' in a lot of your objects (user address in user, office address in office object and others) and you want to reuse the same address rules in all places. You have two ways: write custom rule 'valid_address' or assign rules to a variable and just reuse the variable. The first way requires much time and coding. Moreover, you cannot save new rule implementation is serialzed JSON file. The second way is much easier and you can store rule implemetation in JSON file but you cannot store user validation rules. Moreover, the second way does not allow you to redefine error code for address.
From v0.4 you have the third way - rule aliasing.
You can register aliases for complex rules. The way how you register an alias depends on the implementation but the way how use and describe it is covered by the specification. This appoach allows you store aliases in serialized JSON files and then use them across different implementations.
// Aliasing pseudo code
validator.register_rule_alias({
name: 'valid_address',
rules: { nested_object: {
country: 'required',
city: 'required',
zip: 'positive_integer'
}}
});
validator.register_rule_alias( {
name: 'adult_age',
rules: [ 'positive_integer', { min_number: 18 } ]
});
// Usage
{
name: 'required',
age: ['required', 'adult_age' ],
address: ['required', 'valid_address']
}
Moreover, you can add own error codes for the rules
For example
// Aliasing pseudo code
validator.register_rule_alias({
name: 'valid_address',
rules: { nested_object: {
country: 'required',
city: 'required',
zip: 'positive_integer'
}},
error: 'WRONG_ADDRESS'
});
and if validation of address fails you will have the next error
{
address: 'WRONG_ADDRESS'
}
Requirements to implementation
- Your implementation should support all validation rules described in "Validation Rules"
- Your implementation should support "Rules aliasing"
- Your implementation should return error codes descibed in the specification
- It should be easy to implement own rules
- Please, use provided "test_suite" to ensure that your implementation works correctly
- Added not_empty_list rule with test cases
- Added filter rules (trim, to_lc, to_uc)
- Added special rules (url, iso_date)
- Added filter rules (remove, leave_only)
- Add flags 'i' flag support to the "like" rule
- Introduces new syntax for "one_of" and "list_of" rules. (See "Syntax changes for 'one_of' and 'list_of' rules")
- Rules aliasing (with custom errors)
Old syntax {one_of: [['val1', 'val2']]} was hard to remember for many people. The idea was that list of allowed values should be passed as array reference. So, {one_of: [['val1', 'val2']]} becomes one_of(['val1', 'val2']) but it is not always clear for users. Therefore, it was decided to introduce a new syntax. Now you can write {one_of: ['val1', 'val2']} that becomes one_of('val1', 'val2'). The main problem with it that you do not know how many arguments will be passed to 'one_of'. Moreover, you should support both syntaxes for backward compatibility (test suite contains tests for both cases). But it was decided that "one_of" and "list_of" rules can handle both syntaxes by themselves.
- Describe internals with detailed step-by-step example
- Write developers guide
Copyright 2012 Viktor Turskyi.
This program is free software; you can redistribute it and/or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at:
http://www.perlfoundation.org/artistic\_license\_2\_0
Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License. By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license.
If your Modified Version has been derived from a Modified Version made by someone other than you, you are nevertheless required to ensure that your Modified Version complies with the requirements of this license.
This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder.
This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement, then this Artistic License to you shall terminate on the date that such litigation is filed.
Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.