From 1363e112698c186427bf1689bfa861b97cea0357 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Fri, 14 Mar 2025 06:57:59 +0100 Subject: [PATCH] Validate: allow array schemas + defer known_keys hash creation Doesn't allow multiple 'func' options yet, needs work. --- FU/Validate.pm | 44 +++++++++++++++++++++++++++++--------------- t/validate.t | 5 ++++- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/FU/Validate.pm b/FU/Validate.pm index fd180b8..54b3c77 100644 --- a/FU/Validate.pm +++ b/FU/Validate.pm @@ -76,7 +76,7 @@ our %default_validations = ( uint => { _reg $re_uint }, # implies num min => sub($min) { +{ num => 1, func => sub { $_[0] >= $min ? 1 : { expected => $min, got => $_[0] } } } }, max => sub($max) { +{ num => 1, func => sub { $_[0] <= $max ? 1 : { expected => $max, got => $_[0] } } } }, - range => sub { +{ min => $_[0][0], max => $_[0][1] } }, + range => sub { [ min => $_[0][0], max => $_[0][1] ] }, ascii => { _reg qr/^[\x20-\x7E]*$/ }, sl => { _reg qr/^[^\t\r\n]+$/ }, @@ -99,10 +99,12 @@ our %default_validations = ( # sub _compile($schema, $validations, $rec) { my(%top, @val); - my @keys = keys $schema->{keys}->%* if $schema->{keys}; - for my($name, $val) (%$schema) { + for my($name, $val) (ref $schema eq 'ARRAY' ? @$schema : %$schema) { if ($builtin{$name}) { + confess "Invalid value for 'type': $val" if $name eq 'type' && !$type_vals{$val}; + confess "Invalid value for 'missing': $val" if $name eq 'missing' && !$missing_vals{$val}; + confess "Invalid value for 'unknown': $val" if $name eq 'unknown' && !$unknown_vals{$val}; $top{$name} = $schema->{$name}; next; } @@ -117,6 +119,8 @@ sub _compile($schema, $validations, $rec) { push @val, $v; } + my @keys = keys $top{keys}->%* if $top{keys}; + for my ($n,$t) (qw/keys hash unknown hash values array sort array unique array/) { next if !exists $top{$n}; confess "Incompatible types, the schema specifies '$top{type}' but the '$n' validation implies '$t'" if $top{type} && $top{type} ne $t; @@ -132,8 +136,8 @@ sub _compile($schema, $validations, $rec) { exists $t->{$_} and !exists $top{$_} and $top{$_} = delete $t->{$_} for qw/default onerror trim type scalar unknown missing sort unique/; - push @keys, keys %{ delete $t->{known_keys} }; - push @keys, keys %{ $t->{keys} } if $t->{keys}; + push @keys, delete($t->{known_keys})->@* if $t->{known_keys}; + push @keys, keys $t->{keys}->%* if $t->{keys}; } # Compile sub-schemas @@ -141,24 +145,20 @@ sub _compile($schema, $validations, $rec) { $top{values} = __PACKAGE__->compile($top{values}, $validations) if $top{values}; $top{validations} = \@val; - $top{known_keys} = { map +($_,1), @keys }; + $top{known_keys} = \@keys; \%top; } sub compile($pkg, $schema, $validations={}) { return $schema if $schema isa __PACKAGE__; - my $c = _compile $schema, $validations, 64; $c->{type} //= 'scalar'; $c->{missing} //= 'create'; $c->{trim} //= 1 if $c->{type} eq 'scalar'; $c->{unknown} //= 'remove' if $c->{type} eq 'hash'; - - confess "Invalid value for 'type': $c->{type}" if !$type_vals{$c->{type}}; - confess "Invalid value for 'missing': $c->{missing}" if !$missing_vals{$c->{missing}}; - confess "Invalid value for 'unknown': $c->{unknown}" if exists $c->{unknown} && !$unknown_vals{$c->{unknown}}; + $c->{known_keys} = { map +($_,1), $c->{known_keys}->@* } if $c->{known_keys}; delete $c->{default} if ref $c->{default} eq 'SCALAR' && ${$c->{default}} eq 'required'; @@ -420,10 +420,11 @@ validation. These are documented in L below. =head1 SCHEMA DEFINITION -A schema is a hashref, each key is the name of a built-in option or of a -validation to be performed. None of the options or validations are required, -but some built-ins have default values. This means that the empty schema C<{}> -is actually equivalent to: +A schema is an arrayref or hashref, where each key is the name of a built-in +option or of a validation to be performed and the values are the arguments to +those validations. None of the options or validations are required, but some +built-ins have default values. This means that the empty schema C<{}> is +actually equivalent to: { type => 'scalar', trim => 1, @@ -431,6 +432,19 @@ is actually equivalent to: missing => 'create', } +Built-in options are always validated in a fixed order, but the order in which +standard and custom validations are performed is random when the schema is +given as a hashref. This is rarely a problem, but it can in some cases affect +the returned error message or whether a later validation will receive data +normalized by a previous validation. An arrayref can be used to enforce a +validation order: + + [ enum => [1, 2, 'a'], int => 1 ] + +Or to use the same validation multiple times: + + [ regex => qr/^a/, regex => qr/z$/ ] + =head2 Built-in options =over diff --git a/t/validate.t b/t/validate.t index b01352b..2f8d8d9 100644 --- a/t/validate.t +++ b/t/validate.t @@ -131,6 +131,9 @@ f { type => 'hash', length => 1, unknown => 'pass' }, {qw/1 a 2 b/}, { validatio t { type => 'hash', length => 1, keys => {a => {missing=>'ignore'}, b => {missing=>'ignore'}} }, {a=>1}, {a=>1}; t { regex => '^a' }, 'abc', 'abc'; # XXX: Can't use qr// here because t() does dclone(). The 'hex' test covers that case anyway. f { regex => '^a' }, 'cba', { validation => 'regex', regex => '^a', got => 'cba' }, "failed validation 'regex'"; +t [ regex => '^a', regex => 'z$' ], 'abcxyz', 'abcxyz'; +f [ regex => '^a', regex => 'z$' ], 'bcxyz', { validation => 'regex', regex => '^a', got => 'bcxyz' }, "failed validation 'regex'"; +f [ regex => '^a', regex => 'z$' ], 'abcxy', { validation => 'regex', regex => 'z$', got => 'abcxy' }, "failed validation 'regex'"; t { enum => [1,2] }, 1, 1; t { enum => [1,2] }, 2, 2; f { enum => [1,2] }, 3, { validation => 'enum', expected => [1,2], got => 3 }, "failed validation 'enum'"; @@ -205,7 +208,7 @@ t { range => [1,2] }, 1, 1; t { range => [1,2] }, 2, 2; f { range => [1,2] }, 0.9, { validation => 'range', error => { validation => 'min', expected => 1, got => 0.9 } }, "validation 'range': failed validation 'min'"; f { range => [1,2] }, 2.1, { validation => 'range', error => { validation => 'max', expected => 2, got => 2.1 } }, "validation 'range': failed validation 'max'"; -#t { range => [1,2] }, 'a', 'a', { validation => 'range', error => { validation => 'max', error => nerr 'a' } }; # XXX: Error validation type depends on evaluation order +f { range => [1,2] }, 'a', { validation => 'range', error => { validation => 'min', error => (nerr 'a')[0] } }, "validation 'range': validation 'min': failed validation 'num'"; # email template use utf8;