NAME
Data::Tubes::Plugin::Validator
DESCRIPTION
This module contains factory functions to generate tubes that ease validation of records.
the input record MUST be a hash reference;
one field in the hash (according to factory argument
output
, set tovalidation
by default) is set to the output of the validation operation.
The factory functions below have two names, one starting with validate_
and the other without this prefix. They are perfectly equivalent to each other, whereas the short version can be handier e.g. when using tube
or pipeline
from Data::Tubes.
FUNCTIONS
- with_subs
- validate_with_subs
-
$tube = with_subs(@validators); # OR $tube = with_subs(@validators, \%args);
validate record according to provided
@validators
.Items in
@validators
can be either sub references or array references, as explained below. An optional hash reference at the end can carry options, see below for their explanation.A validator basically boils down to a sub reference that is called to perform the validation. It can be either provided directly as an item in
@validators
, or embedded in an array reference, prefixed with a name and with optional additional parameters. Example:$tube = with_subs( sub { $_[0]{foo} =~ /bar|baz/ }, # straight sub ref [ 'Number should be even', sub { $_[0]{number} % 2 == 0 }, ], [ 'Name of something else', sub { ... }, @parameters ], );
The validator function will be called in list context, like this:
my @outcome = $validator->( $target, # what pointed by "input", or the whole record $record, # the whole record, if necessary \%args, # args passed to the factory @parameters, # anything sub in the array ref version );
The validator can:
return the empty list, in which case the validation is considered failed;
return a single value, that represents the outcome of the validation. Anything considered false by Perl means that the validation failed, otherwise the validation is considered a success;
return more values, the first representing the outcome of the validation as in the previous bullet, the following ones things that you want to track as the outcome of the validation (e.g. some explanation of what went wrong with the validation).
If one of the validators throws an exception, this will not be trapped unless
wrapper
is set properly. See below if you want to catch exceptions and transform them into failed validation steps.All validations are performed in the order provided in
@validators
, independently of whether they succeed or fail. This is by design, so that you can provide a thorough feedback about what you think is wrong with the input data.Validation outcomes are collected into an array of arrays that is eventually referenced by the record provided as output (which is the same as the input, only augmented). By default this array of arrays is referenced by key
validation
, but you can control the key via optionoutput
.Normally, only failed validations are collected in the array, so that you can easily check if validation was successful at a later stage. You can decide to collect all outcomes via option
keep_positives
.By default, if the validation collection procedure does not collect anything (i.e. all validations were successful and
keep_positives
is false), the output key is set toundef
, so that you can check for validation errors very quickly instead of checking the number of items in the array. If you prefer to receive an empty array instead, you can set optionkeep_empty
.You can wrap the call to all your validators via an optional
wrapper
sub reference. This means that the following call will be used instead:my @outcome = $wrapper->( $validator, # the validation function $target, # what pointed by "input", or the whole record $record, # the whole record, if necessary \%args, # args passed to the factory @parameters, # anything sub in the array ref version );
In this case, your
wrapper
function will be responsible for calling$validator
in the right way. You can use this e.g. to perform some adaptation of interface for either the input or the output of the validation sub. As a matter of fact, in this case$validator
is not even required to be a sub reference.In addition to setting
wrapper
to a sub reference, you can also set it to the stringtry
. This will wrap the call to the validator in atry
/catch
using Try::Tiny, which you are supposed to have installed independently.Allowed arguments are:
input
-
the name of the input field in the record. Defaults to
structured
, in the assumption that you will want to perform validation after parsing, but you can of course set it to whatever you want. If you set it toundef
, the whole input record will be considered the$target
for the validation. Keep in mind that each validator will always receive also a reference to the$record
as the second argument anyway; keep_empty
-
if all validators succeed and
keep_positives
below is false, the overall outcome of the validation process will be an empty array. This option allows you to control whether you want an empty array asoutput
in this case, or you prefer to receive a false value for quicker identification of no validation errors condition. Defaults to0
, i.e. a false value, meaning that you will receive an undefined value inoutput
in case all validations were successful; keep_positives
-
validations that are successful are normally discarded, as you are assumed to be interested into failures most. If you want an account of all the validation steps, instead, you can set this flag to a true value. Defaults to
0
, a false value, meaning that positive validations are discarded; name
-
the name of the tube, useful when debugging. Defaults to
validate with subs
; output
-
the name of the output field in the output record. Defaults to
validation
. wrapper
-
a subroutine to wrap each call to a validator. In this case,
with_subs
will call the wrapper instead, passing as the first parameter the validator, then the list of parameter it would have passed to the validator itself.You can also pass the special value
try
, that allows you to set the following wrapper subroutine equivalent:use Try::Tiny; sub { my ($validator, @parameters) = @_; return try { $validator->(@parameters); } catch { (0, $_); }; }
except that Try::Tiny is loaded dynamically at runtime and no function is imported. This allows you to turn exceptions into failed validations (note that the first item in the expression inside the
catch
part is0
, i.e. a failed validation) where the exception iteself is passed as additional "reason" that is eventually collected in the outcome.
A few examples should be of help now.
First, an example with validators that all return a true or false value, hence there is nothing to trap:
my $v = with_subs( sub { $_[0]{foo} =~ /bar|baz/ }, ['is-even' => sub { $_[0]{number} % 2 == 0 }], ['in-bounds' => sub { $_[0]{number} >= 10 && $_[0]{number} <= 21}] ); my $o1 = $v->({structured => {foo => 'bar', number => 12}}); my $o2 = $v->({structured => {foo => 'bar', number => 13}}); my $o3 = $v->({structured => {foo => 'hey', number => 3}});
In all cases the output record contains a new
validation
key, pointing to:$o1
anundef
value$o2
an array reference like this:[ ['is-even', ''] ]
because the test
is-even
fails returning an empty string$o3
an array reference like this:[ ['validator-0', 0], # empty list transformed into "0" ['is-even', ''], # empty string from validator ['in-bound', ''] # empty string from validator ]
As you can see, in the case of the first test a name is automatically generated based on the index of the test in the list of validators.
Here's an example for trapping exceptions:
my $v= with_subs( sub { $_[0]{foo} =~ /bar|baz/ }, ['is-even' => sub { ($_[0]{number} % 2 == 0) or die "odd\n" }], ['in-bounds' => sub { $_[0]{number} >= 10 or die "too low\n" }], {wrapper => 'try'}, ); my $o4 = $v->({structured => {foo => 'bar', number => 13}}); my $o5 = $v->({structured => {foo => 'hey', number => 3}});
Again, you will get a
validation
key in each output record, like this:$o4
-
only the first test fails in this case, so this is what you get:
[ ['is-even', 0, "odd\n"] ]
$o5
-
all three tests fail, two with exception, leading to this:
[ ['validator-0', 0], # as before ['is-even', 0, "odd\n"], # exception to failure ['in-bound', 0, "too low"] # exception to failure ]
You hopefully get the idea at this point.
It's important to always remember the difference between the following validators:
sub { ($_[0]{number} % 2 == 0) or die "odd\n" }; sub { die "odd\n" if $_[0]{number} % 2 };
The second validator always fails: it either throws an exception, or returns a false value. This is not the case with the first one. Always remember to return a true value from your validators, like this:
sub { die "odd\n" if $_[0]{number} % 2; 1 }
(Yes, this actually happened while writing the tests...)
BUGS AND LIMITATIONS
Report bugs either through RT or GitHub (patches welcome).
AUTHOR
Flavio Poletti <polettix@cpan.org>
COPYRIGHT AND LICENSE
Copyright (C) 2016 by Flavio Poletti <polettix@cpan.org>
This module is free software. You can redistribute it and/or modify it under the terms of the Artistic License 2.0.
This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.