=for HTML Tests CPAN version =head1 NAME Validator::LIVR - Lightweight validator supporting Language Independent Validation Rules Specification (LIVR) =head1 SYNOPSIS use Validator::LIVR; my $validator = Validator::LIVR->new({ name => 'required', email => [ 'required', 'email' ], gender => { one_of => [ 'male', 'female' ] }, phone => { max_length => 10 }, password => [ 'required', { min_length => 10 } ], password2 => { equal_to_field => 'password' }, }); if ( my $valid_data = $validator->validate($user_data) ) { save_user($valid_data); } else { my $errors = $validator->get_errors(); # { email => 'WRONG_EMAIL', password => 'TOO_SHORT' } } =head1 DESCRIPTION LIVR rules are plain data: a hash that maps field names to rules. They are easy to read, and because the same rules work in L, one rule set can check a form in the browser with JavaScript and again on the server with Perl. This module implements L and passes the test suite of the specification. =over 4 =item * B that you can keep in JSON and share with other services. =item * B, not just the first one. =item * B A field without rules, such as an unexpected C, never reaches your code. =item * B: objects, lists, lists of objects and objects of several types. =item * B, such as C or C. Turn them into messages in any language you need. =item * B with modifiers, such as C, C and C. =item * B, as aliases of existing rules or as Perl code. =item * B: only modules that come with Perl. =back Use it for user input, configs, API requests and responses, or any other data that has to have a known shape. =head1 A QUICK TOUR =head2 Writing rules A rule set maps each field to its rules. A rule is written in one of these ways: { name => 'required', # a rule without arguments bio => { max_length => 1000 }, # a rule with one argument nick => { length_between => [ 3, 20 ] }, # a rule with several arguments email => [ 'required', 'email' ], # several rules } Several rules of a field are applied in order, and the field stops at its first error. A hash is always one rule, so two rules with arguments are two hashes: C<< [ { min_length => 3 }, { max_length => 20 } ] >>. Most rules accept empty values: C and the empty string. So a field is optional until you add C: { age => 'positive_integer', # can be missing id => [ 'required', 'positive_integer' ], # must be present } =head2 Validating nested data Metarules describe the structure of the data, and errors have the same shape as the data: my $validator = Validator::LIVR->new({ order_id => [ 'required', 'positive_integer' ], address => [ 'required', { nested_object => { country => [ 'required', { one_of => [ 'Ukraine', 'USA' ] } ], city => 'required', zip => 'positive_integer', } } ], products => [ 'required', 'not_empty_list', { list_of_objects => { product_id => [ 'required', 'positive_integer' ], quantity => [ 'required', { min_number => 1 } ], } } ], tags => { list_of => [ 'string', { max_length => 10 } ] }, }); For an order with an unknown country, a second product with a quantity of 0 and a second tag that is too long, C returns: { address => { country => 'NOT_ALLOWED_VALUE' }, products => [ undef, { quantity => 'TOO_LOW' } ], tags => [ undef, 'TOO_LONG' ], } =head2 Cleaning data Modifiers change the value for the next rules and for the output: my $validator = Validator::LIVR->new({ email => [ 'trim', 'required', 'email', 'to_lc' ], phone => { leave_only => '0123456789' }, role => [ { default => 'user' }, { one_of => [ 'user', 'admin' ] } ], }); $validator->validate({ email => ' John@Example.COM ', phone => '+38 (050) 123-45-67' }); # { email => 'john@example.com', phone => '380501234567', role => 'user' } To trim every string of the input before validation, turn on auto trim for one validator with C<< Validator::LIVR->new( $rules, 1 ) >>, or for all new validators with C<< Validator::LIVR->default_auto_trim(1) >>. =head2 Sharing rules with other languages Rules are plain data, so they can live in a JSON file: { "email": [ "required", "email" ], "password": [ "required", { "min_length": 10 } ] } use JSON::PP qw(decode_json); my $validator = Validator::LIVR->new( decode_json($json_rules) ); The same file works with L in JavaScript, and with the L for PHP, Python, Go, Java, Ruby, Erlang, Lua, Raku and Swift. You can try rules in the L. =head2 Adding your own rules The simplest way is an alias: a new name for a combination of rules, with its own error code. Aliases are a part of the specification, so other implementations understand them too. Validator::LIVR->register_aliased_default_rule({ name => 'adult_age', rules => [ 'positive_integer', { min_number => 18 } ], error => 'WRONG_AGE', }); my $validator = Validator::LIVR->new({ age => [ 'required', 'adult_age' ] }); For anything else, write the rule in Perl, see L. =head1 RULES The built-in rules, grouped as in the specification. Each group links to its page in the specification, which has the details and edge cases. A rule that expects a string or a number returns C for a hash or a list. A rule that expects a hash or a list returns C for anything else. =head2 Common rules L =over 4 =item C The field is present and not empty: not C and not C<''>. Error: C. =item C The field can be missing, but when it is there, it is not C<''>. Error: C. =item C The field is a list with at least one item. Errors: C, C. =item C The field is a hash with any content. Error: C. =back =head2 String rules L =over 4 =item C Any string or number. Error: C. =item C<< { eq => $value } >> The value is equal to C<$value>. Error: C. =item C<< { one_of => [ @values ] } >> The value is equal to one of C<@values>. Error: C. =item C<< { min_length => $length } >> The value has at least C<$length> characters. Error: C. =item C<< { max_length => $length } >> The value has at most C<$length> characters. Error: C. =item C<< { length_equal => $length } >> The value has exactly C<$length> characters. Errors: C, C. =item C<< { length_between => [ $min, $max ] } >> The value has from C<$min> to C<$max> characters. Errors: C, C. =item C<< { like => $regex } >>, C<< { like => [ $regex, 'i' ] } >> The value matches the regular expression. The C flag makes it case insensitive. Error: C. The rule is experimental in the specification. The syntax of regular expressions differs between languages, so stick to the common features when you share rules. =back =head2 Numeric rules L =over 4 =item C An integer, such as C<-5> or C<42>. Error: C. =item C An integer greater than zero. Error: C. =item C A number, such as C<-1.5> or C<42>. Error: C. =item C A number greater than zero. Error: C. =item C<< { min_number => $min } >> A number that is not less than C<$min>. Errors: C, C. =item C<< { max_number => $max } >> A number that is not greater than C<$max>. Errors: C, C. =item C<< { number_between => [ $min, $max ] } >> A number from C<$min> to C<$max>. Errors: C, C, C. =back =head2 Special rules L =over 4 =item C An email address, such as C, without a name, comments or spaces around it. The domain has a TLD or is an IP address in brackets. Error: C. =item C An C or C URL, such as C. The host is a domain name, such as C or C, or an IPv4 address. Domain names in other languages work in decoded strings, not in UTF-8 bytes. Error: C. =item C A date as C that exists in the calendar: C<2024-02-29> is valid, C<2025-02-29> is not. Error: C. =item C<< { equal_to_field => $field } >> The value is equal to the value of another field, such as a password confirmation. Error: C. =back =head2 Metarules L Metarules validate nested data with their own rule sets. Their errors are nested too: a hash of errors for an object, and a list for a list, with C for the valid items. =over 4 =item C<< { nested_object => \%rules } >> A hash validated with C<\%rules>. =item C<< { list_of => \@rules } >>, C<< { list_of => $rule } >> A list, each item validated with the rules. =item C<< { list_of_objects => \%rules } >> A list of hashes, each validated with C<\%rules>. =item C<< { variable_object => [ $selector, { $type => \%rules, ... } ] } >> A hash validated with the rules for the value of its C<$selector> field. The error is C when the hash has no rules for that value. payment => [ 'required', { variable_object => [ type => { card => { type => 'required', card_number => [ 'required', { length_equal => 16 } ], }, iban => { type => 'required', iban => 'required', }, } ] } ], =item C<< { list_of_different_objects => [ $selector, { $type => \%rules, ... } ] } >> A list of hashes, each validated as in C. =item C<< { or => [ $rules1, $rules2, ... ] } >> The value passes at least one of the rule sets. Each rule set is a rule or a list of rules. When all of them fail, the error is the error of the last one. id => { or => [ 'email', 'positive_integer' ] }, =back =head2 Modifiers L Modifiers change the value and never fail. =over 4 =item C Removes the spaces at the start and at the end. =item C Converts to lower case. =item C Converts to upper case. =item C<< { remove => $chars } >> Removes each of the characters in C<$chars>, so C<< { remove => ' -()' } >> turns C<(050) 123-45-67> into C<0501234567>. =item C<< { leave_only => $chars } >> Removes all characters except the ones in C<$chars>. =item C<< { default => $value } >> Sets C<$value> when the field is missing or empty. =back =head1 CLASS METHODS =head2 new my $validator = Validator::LIVR->new( \%rules ); my $validator = Validator::LIVR->new( \%rules, $is_auto_trim ); Creates a validator. With a true C<$is_auto_trim>, it trims all strings of the input, the nested ones too, before validation, so the output is trimmed as well. When C<$is_auto_trim> is C, the value set with L is used. =head2 register_default_rules Validator::LIVR->register_default_rules( my_rule => \&my_rule_builder, other_rule => \&other_rule_builder, ); Adds rules or replaces existing ones for all validators created after the call. See L for how to write a rule builder. =head2 register_aliased_default_rule Validator::LIVR->register_aliased_default_rule({ name => 'valid_address', rules => { nested_object => { country => 'required', city => 'required', zip => 'positive_integer', } }, error => 'WRONG_ADDRESS', # optional }); Adds an alias for all validators created after the call. Then the alias works as any other rule: { address => [ 'required', 'valid_address' ] } Without C, the alias returns the error of its rules. =head2 get_default_rules my $rule_builders = Validator::LIVR->get_default_rules(); Returns a hashref that maps rule names to the rule builders that new validators get. =head2 default_auto_trim Validator::LIVR->default_auto_trim(1); Turns auto trim on or off for validators created after the call. See L. =head1 OBJECT METHODS =head2 validate my $valid_data = $validator->validate( \%input ); Returns a hashref with the valid data: only the fields that have rules, with the changes made by modifiers. On errors returns C, and L returns the errors. =head2 get_errors my $errors = $validator->get_errors(); Returns the errors of the last L call, or C when it passed. The errors are a hashref that maps fields to error codes, with nested errors for metarules: { email => 'WRONG_EMAIL', address => { zip => 'NOT_POSITIVE_INTEGER' }, products => [ undef, { quantity => 'TOO_LOW' } ], } When the input is not a hashref, the errors are the string C. =head2 register_rules $validator->register_rules( my_rule => \&my_rule_builder, ... ); Adds rules or replaces existing ones, as L does, for this validator only. Returns the validator, so the call can be chained. =head2 register_aliased_rule $validator->register_aliased_rule({ name => 'strong_password', rules => { min_length => 12 }, error => 'WEAK_PASSWORD', }); Adds an alias, as L does, for this validator only. Returns the validator. =head2 get_rules my $rule_builders = $validator->get_rules(); Returns a hashref that maps rule names to the rule builders of this validator. =head2 prepare my $validator = Validator::LIVR->new($rules)->prepare(); Builds the validators of all rules and returns the validator. L does it on the first call, so you need C only to find mistakes early. It dies with C when a rule is unknown, and with C when a hash has several rules. Register your rules before C or the first L. =head1 WRITING RULES IN PERL A rule is a builder: a sub that gets the arguments of the rule and returns a validator sub. The builder is called once, when the validator is prepared. The validator sub is called for every value. Here is how C could be written: use Scalar::Util qw(looks_like_number); Validator::LIVR->register_default_rules( max_number => sub { my ( $max_number, $rule_builders ) = @_; return sub { my ( $value, $all_values, $output_ref ) = @_; # Empty values are the job of "required" return if !defined($value) || $value eq ''; return 'FORMAT_ERROR' if ref($value); return 'NOT_NUMBER' if !looks_like_number($value); return 'TOO_HIGH' if $value > $max_number; return; }; }); The builder gets the arguments of the rule, then a hashref of all rule builders of the validator: 'my_rule' # ( $rule_builders ) { my_rule => $arg } # ( $arg, $rule_builders ) { my_rule => [ $arg ] } # ( $arg, $rule_builders ) { my_rule => [ $arg1, $arg2 ] } # ( $arg1, $arg2, $rule_builders ) The validator sub gets: =over 4 =item C<$value> The value of the field, as the previous rules of the field left it. =item C<$all_values> The hash that has the field, to compare it with other fields, as C does. Inside C, C and aliases, rules check the value on its own, so this hash has no other fields. =item C<$output_ref> A reference to the output value. Assign to it to change the value, as modifiers do. =back It returns nothing when the value is valid, and an error code when it is not. A modifier sets the output and returns nothing: Validator::LIVR->register_default_rules( to_ucfirst => sub { return sub { my ( $value, undef, $output_ref ) = @_; return if !defined($value) || ref($value) || $value eq ''; $$output_ref = ucfirst($value); return; }; }); With C<$rule_builders>, a rule can validate nested data with the same rules, as metarules do: my $validator = Validator::LIVR->new($rules)->register_rules(%$rule_builders)->prepare(); The built-in rules are short and make good examples: L, L, L, L, L and L. =head1 SEE ALSO =over 4 =item * L, with all rules, L and a L. =item * L, with the specification and its test suite. =item * L in other languages. =back =head1 BUGS AND SUPPORT Please report bugs and ideas in L. The source is at L. You can read this documentation with C, or on L. =head1 AUTHOR Viktor Turskyi, C<< >> =head1 LICENSE AND COPYRIGHT Copyright 2012-2026 Viktor Turskyi. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License (2.0). You may obtain a copy of the full license at: L 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. =cut