Validate: Rename "values"->"elems", repurpose "values" to validate hash values

I'm breaking stuff left and right while I still can.

Idea: "key_names" validation?

Idea: "tuple" validation that works like "keys" but for arrays.
  (i.e. { tuple => { $index => $schema } }, could make "missing" and
  "unknown" work for arrays, too)
This commit is contained in:
Yorhel 2025-03-14 16:44:08 +01:00
parent fa24ca53e3
commit 3fad7feec3
2 changed files with 82 additions and 51 deletions

View file

@ -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<password> field must be equivalent to that
of a I<confirm_password> 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<keys>, 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<keys> option. Possible
values are I<remove> to remove unknown keys from the output data (this is the
default), I<reject> to return an error if there are unknown keys in the input,
or I<pass> 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<pass> to pass through any unknown keys to the output data. Values for
passed-through keys are only validated when the I<values> option is set,
otherwise they are passed through as-is. This option has no effect when the
I<keys> option is never set, in that case all values are always passed through.
In the case of I<reject>, the error object will look like:
@ -549,7 +577,8 @@ undef), I<reject> to return an error if the option is missing or I<ignore> to
leave the key out of the returned data.
The default is I<create>, but if no I<default> option is set for this key then
that is effectively the same as I<reject>.
that is effectively the same as I<reject>. Values created through I<create> are
still validated through I<values> if that has been set.
In the case of I<reject>, the error object will look like:
@ -557,15 +586,15 @@ In the case of I<reject>, 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<compile()>.
Failure is reported in a similar fashion to I<keys>:
{ 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<type> option.
If a custom validation does not specify a I<type> option (and no type is
implied by another validation such as I<keys> or I<values>), then the
implied by another validation such as I<keys> or I<elems>), 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<func> option is validated separately for each custom validation.
Multiple I<keys> and I<values> validations are merged into a single validation.
So if you have multiple custom validations that set the I<values> option, a
Multiple I<keys> and I<elems> validations are merged into a single validation.
So if you have multiple custom validations that set the I<elems> option, a
single combined schema is created that validates all array elements. The same
applies to I<keys>: if the same key is listed in multiple custom validations,
then the key must conform to all schemas. With respect to the I<unknown>

View file

@ -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';