diff --git a/FU/Validate.pm b/FU/Validate.pm index feeabb3..8d8db91 100644 --- a/FU/Validate.pm +++ b/FU/Validate.pm @@ -13,15 +13,18 @@ my %builtin = map +($_,1), qw/ default onerror trim - values scalar sort unique - keys unknown missing + elems scalar sort unique + keys values unknown missing func /; my %type_vals = map +($_,1), qw/scalar hash array any/; my %unknown_vals = map +($_,1), qw/remove reject pass/; my %missing_vals = map +($_,1), qw/create reject ignore/; -my %implied_type = qw/keys hash unknown hash values array sort array unique array/; +my %implied_type = qw/ + keys hash values hash unknown hash + elems array sort array unique array scalar array +/; my %sort_vals = ( str => sub($x,$y) { $x cmp $y }, num => sub($x,$y) { $x <=> $y }, @@ -119,9 +122,9 @@ sub _compile($schema, $custom, $rec, $top, $validations=$top->{validations}) { $top->{type} = $type; } - if ($name eq 'values') { - $top->{values} ||= _new; - _compile($val, $custom, $rec-1, $top->{values}); + if ($name eq 'elems' || $name eq 'values') { + $top->{$name} ||= _new; + _compile($val, $custom, $rec-1, $top->{$name}); next; } @@ -173,35 +176,51 @@ sub compile($pkg, $schema, $custom={}) { } -sub _validate_keys { - my @err; - for my ($k, $s) ($_[0]{keys}->%*) { - if (!exists $_[1]{$k}) { - next if $s->{missing} && $s->{missing} eq 'ignore'; - return { validation => 'missing', key => $k } if $s->{missing} && $s->{missing} eq 'reject'; - $_[1]{$k} = ref $s->{default} eq 'CODE' ? $s->{default}->() : $s->{default} // undef; - next if exists $s->{default}; - } +sub _validate_hash { + my $c = $_[0]; - my $r = _validate($s, $_[1]{$k}); - if ($r) { - $r->{key} = $k; - push @err, $r; + if ($c->{keys}) { + my @err; + for my ($k, $s) ($c->{keys}->%*) { + if (!exists $_[1]{$k}) { + next if $s->{missing} && $s->{missing} eq 'ignore'; + return { validation => 'missing', key => $k } if $s->{missing} && $s->{missing} eq 'reject'; + $_[1]{$k} = ref $s->{default} eq 'CODE' ? $s->{default}->() : $s->{default} // undef; + next if exists $s->{default}; + } + + my $r = _validate($s, $_[1]{$k}); + if ($r) { + $r->{key} = $k; + push @err, $r; + } } + return { validation => 'keys', errors => [ sort { $a->{key} cmp $b->{key} } @err ] } if @err; + } + + if ($c->{values}) { + my @err; + for my ($k, $v) ($_[1]->%*) { + my $r = _validate($c->{values}, $v); + if ($r) { + $r->{key} = $k; + push @err, $r; + } + } + return { validation => 'values', errors => [ sort { $a->{key} cmp $b->{key} } @err ] } if @err; } - return { validation => 'keys', errors => [ sort { $a->{key} cmp $b->{key} } @err ] } if @err; } -sub _validate_values { +sub _validate_elems { my @err; for my $i (0..$#{$_[1]}) { - my $r = _validate($_[0]{values}, $_[1][$i]); + my $r = _validate($_[0]{elems}, $_[1][$i]); if ($r) { $r->{index} = $i; push @err, $r; } } - return { validation => 'values', errors => \@err } if @err; + return { validation => 'elems', errors => \@err } if @err; } @@ -275,14 +294,14 @@ sub _validate_input { # Each branch below makes a shallow copy of the hash, so that further # validations can perform in-place modifications without affecting the # input. - if (!$c->{unknown} || $c->{unknown} eq 'remove') { + if (!$c->{keys} || ($c->{unknown} && $c->{unknown} eq 'pass')) { + $_[1] = { $_[1]->%* }; + } elsif (!$c->{unknown} || $c->{unknown} eq 'remove') { $_[1] = { map +($_, $_[1]{$_}), grep $c->{keys}{$_}, keys $_[1]->%* }; - } elsif ($c->{unknown} && $c->{unknown} eq 'reject') { + } else { my @err = grep !$c->{keys}{$_}, keys $_[1]->%*; return { validation => 'unknown', keys => \@err, expected => [ sort keys $c->{keys}->%* ] } if @err; $_[1] = { $_[1]->%* }; - } else { - $_[1] = { $_[1]->%* }; } } elsif ($type eq 'array') { @@ -294,8 +313,8 @@ sub _validate_input { # No need to do anything here. } - ($c->{keys} && &_validate_keys) || - ($c->{values} && &_validate_values) || + ($type eq 'hash' && &_validate_hash) || + ($c->{elems} && &_validate_elems) || &_validate_rec || ($type eq 'array' && &_validate_array) } @@ -340,8 +359,9 @@ sub errors($e, $prefix='') { my $val = $e->{validation}; my $p = $prefix ? "$prefix: " : ''; $val eq 'keys' ? map errors($_, $prefix.'.'._fmtkey($_->{key})), $e->{errors}->@* : + $val eq 'values' ? map errors($_, $prefix.'.'._fmtkey($_->{key})), $e->{errors}->@* : $val eq 'missing' ? $prefix.'.'._fmtkey($e->{key}).': required key missing' : - $val eq 'values' ? map errors($_, $prefix."[$_->{index}]"), $e->{errors}->@* : + $val eq 'elems' ? map errors($_, $prefix."[$_->{index}]"), $e->{errors}->@* : $val eq 'unique' ? $prefix."[$e->{index_b}] value '"._fmtval($e->{value_a})."' duplicated" : $val eq 'required' ? "${p}required value missing" : $val eq 'type' ? "${p}invalid type, expected '$e->{expected}' but got '$e->{got}'" : @@ -374,10 +394,10 @@ validate the format and the structure of the data, but it does not support validations that depend on other input values. For example, it is not possible to specify that the contents of a I field must be equivalent to that of a I field, but you can specify that both fields need to be -filled out. Recursive data structures are not supported. There is also no -built-in support for validating hashes with dynamic keys or arrays where not -all elements conform to the same schema. These could technically still be -validated with custom validations, but it won't be as convenient. +filled out. Recursive data structures are not supported. There is also no good +support for validating hashes with dynamic keys or arrays where not all +elements conform to the same schema. These could technically still be validated +with custom validations, but it won't be as convenient. This module is designed to validate any kind of program input after it has been parsed into a Perl data structure. It should not be used to validate function @@ -521,14 +541,22 @@ like: ] } +=item values => $schema + +Implies C<< type => 'hash' >>, set a schema that is used to validate every hash +value. Can be used together with I, in which case values must validate +both this C<$schema> and the schema corresponding to the key. + =item unknown => $option Implies C<< type => 'hash' >>, this option specifies what to do with keys in the input data that have not been defined in the I option. Possible values are I to remove unknown keys from the output data (this is the default), I to return an error if there are unknown keys in the input, -or I to pass through any unknown keys to the output data. Note that the -values for passed-through keys are not validated against any schema! +or I to pass through any unknown keys to the output data. Values for +passed-through keys are only validated when the I option is set, +otherwise they are passed through as-is. This option has no effect when the +I option is never set, in that case all values are always passed through. In the case of I, the error object will look like: @@ -549,7 +577,8 @@ undef), I to return an error if the option is missing or I to leave the key out of the returned data. The default is I, but if no I option is set for this key then -that is effectively the same as I. +that is effectively the same as I. Values created through I are +still validated through I if that has been set. In the case of I, the error object will look like: @@ -557,15 +586,15 @@ In the case of I, the error object will look like: key => 'field' } -=item values => $schema +=item elems => $schema Implies C<< type => 'array' >>, this defines the schema that is applied to -every item in the array. The schema definition may be a bare hashref or a +every element in the array. The schema definition may be a bare hashref or a validator returned by C. Failure is reported in a similar fashion to I: - { validation => 'values', + { validation => 'elems', errors => [ { index => 1, validation => 'required' } ] @@ -626,8 +655,7 @@ All of that may sound complicated, but it's quite easy to use. Here's a few examples: # This describes an array of hashes with keys 'id' and 'name'. - { values => { - type => 'hash', + { elems => { keys => { id => { uint => 1 }, name => {} @@ -641,7 +669,7 @@ examples: # Contrived example: An array of strings, and we want # each string to start with a different character. - { values => { minlength => 1 }, + { elems => { minlength => 1 }, unique => sub { substr $_[0], 0, 1 } } @@ -845,7 +873,7 @@ used in that schema may get input with whitespace around it. All validations used in a schema need to agree upon a single I option. If a custom validation does not specify a I option (and no type is -implied by another validation such as I or I), then the +implied by another validation such as I or I), then the validation should work with every type. It is an error to define a schema that mixes validations of different types. For example, the following throws an error: @@ -859,8 +887,8 @@ error: The I option is validated separately for each custom validation. -Multiple I and I validations are merged into a single validation. -So if you have multiple custom validations that set the I option, a +Multiple I and I validations are merged into a single validation. +So if you have multiple custom validations that set the I option, a single combined schema is created that validates all array elements. The same applies to I: if the same key is listed in multiple custom validations, then the key must conform to all schemas. With respect to the I diff --git a/t/validate.t b/t/validate.t index 7533a02..de37264 100644 --- a/t/validate.t +++ b/t/validate.t @@ -19,7 +19,7 @@ my %validations = ( neverfails => { onerror => 'err' }, doublefunc => [ func => sub { $_[0] == 0 }, func => sub { $_[0] = 2; 1; } ], revnum => { type => 'array', sort => sub($x,$y) { $y <=> $x } }, - uniquelength => { type => 'array', values => { type => 'array' }, unique => sub { scalar @{$_[0]} } }, + uniquelength => { elems => { type => 'array' }, unique => sub { scalar @{$_[0]} } }, person => { type => 'hash', unknown => 'pass', @@ -85,8 +85,8 @@ f { type => 'array' }, 1, { validation => 'type', expected => 'array', got => 's t { type => 'array' }, [], []; t { type => 'array' }, [undef,1,2,{}], [undef,1,2,{}]; t { type => 'array', scalar => 1 }, 1, [1]; -f { type => 'array', values => {} }, [undef], { validation => 'values', errors => [{ index => 0, validation => 'required' }] }, "[0]: required value missing"; -t { type => 'array', values => {} }, [' a '], ['a']; +f { type => 'array', elems => {} }, [undef], { validation => 'elems', errors => [{ index => 0, validation => 'required' }] }, "[0]: required value missing"; +t { type => 'array', elems => {} }, [' a '], ['a']; t { type => 'array', sort => 'str' }, [qw/20 100 3/], [qw/100 20 3/]; t { type => 'array', sort => 'num' }, [qw/20 100 3/], [qw/3 20 100/]; t { revnum => 1 }, [qw/20 100 3/], [qw/100 20 3/]; @@ -97,12 +97,12 @@ f { type => 'array', unique => 1 }, [qw/3 1 3/], { validation => 'unique', index t { uniquelength => 1 }, [[],[1],[1,2]], [[],[1],[1,2]]; f { uniquelength => 1 }, [[],[1],[2]], { validation => 'unique', index_a => 1, value_a => [1], index_b => 2, value_b => [2], key => 1 }, q{[2] value '[1]' duplicated}; t { type => 'array', setundef => 1 }, [], undef; -t { type => 'array', values => { type => 'any', setundef => 1 } }, [[]], [undef]; +t { type => 'array', elems => { type => 'any', setundef => 1 } }, [[]], [undef]; # hashes f { type => 'hash' }, [], { validation => 'type', expected => 'hash', got => 'array' }, "invalid type, expected 'hash' but got 'array'"; f { type => 'hash' }, 'a', { validation => 'type', expected => 'hash', got => 'scalar' }, "invalid type, expected 'hash' but got 'scalar'"; -t { type => 'hash' }, {a=>[],b=>undef,c=>{}}, {}; +t { type => 'hash' }, {a=>[],b=>undef,c=>{}}, {a=>[],b=>undef,c=>{}}; f { type => 'hash', keys => { a=>{} } }, {}, { validation => 'keys', errors => [{ key => 'a', validation => 'required' }] }, '.a: required value missing'; t { type => 'hash', keys => { a=>{missing=>'ignore'} } }, {}, {}; t { type => 'hash', keys => { a=>{default=>undef} } }, {}, {a=>undef}; @@ -120,6 +120,9 @@ t [ keys => { a => {} }, keys => { b => {} } ], {a=>1, b=>2}, {a=>1, b=>2}; f [ keys => { a => {} }, keys => { b => {} } ], {a=>1}, { validation => 'keys', errors => [{ key => 'b', validation => 'required' }] }, '.b: required value missing'; f [ keys => { a => {} }, keys => { a => { int => 1 } } ], {a=>'abc'}, { validation => 'keys', errors => [{ key => 'a', validation => 'int', got => 'abc' }] }, ".a: failed validation 'int'"; +t { values => { int => 1 } }, { a => -1, b => 1 }, { a => -1, b => 1 }; +f { values => { int => 1 } }, { a => undef }, { validation => 'values', errors => [{ key => 'a', validation => 'required' }] }, '.a: required value missing'; + # default validations f { minlength => 3 }, 'ab', { validation => 'minlength', expected => 3, got => 2 }, "failed validation 'minlength'"; t { minlength => 3 }, 'abc', 'abc';