Validate: Rename "scalar" to "accept_scalar" and add "accept_array"

This commit is contained in:
Yorhel 2025-03-16 15:39:10 +01:00
parent f8fe53cba9
commit 3382deba9a
2 changed files with 33 additions and 10 deletions

View file

@ -13,7 +13,8 @@ my %builtin = map +($_,1), qw/
default default
onerror onerror
trim trim
elems scalar sort unique elems sort unique
accept_scalar accept_array
keys values unknown missing keys values unknown missing
func func
/; /;
@ -22,8 +23,9 @@ my %type_vals = map +($_,1), qw/scalar hash array any/;
my %unknown_vals = map +($_,1), qw/remove reject pass/; my %unknown_vals = map +($_,1), qw/remove reject pass/;
my %missing_vals = map +($_,1), qw/create reject ignore/; my %missing_vals = map +($_,1), qw/create reject ignore/;
my %implied_type = qw/ my %implied_type = qw/
accept_array scalar
keys hash values hash unknown hash keys hash values hash unknown hash
elems array sort array unique array scalar array elems array sort array unique array accept_scalar array
/; /;
my %sort_vals = ( my %sort_vals = (
str => sub($x,$y) { $x cmp $y }, str => sub($x,$y) { $x cmp $y },
@ -151,6 +153,7 @@ sub _compile($schema, $custom, $rec, $top, $validations=$top->{validations}) {
if ($builtin{$name}) { if ($builtin{$name}) {
confess "Invalid value for 'missing': $val" if $name eq 'missing' && !$missing_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}; confess "Invalid value for 'unknown': $val" if $name eq 'unknown' && !$unknown_vals{$val};
confess "Invalid value for 'accept_array': $val" if $name eq 'accept_array' && $val && $val ne 'first' && $val ne 'last';
$val = $sort_vals{$val} || confess "Unknown value for 'sort': $val" if $name eq 'sort' && ref $val ne 'CODE'; $val = $sort_vals{$val} || confess "Unknown value for 'sort': $val" if $name eq 'sort' && ref $val ne 'CODE';
$top->{$name} = $val; $top->{$name} = $val;
next; next;
@ -273,6 +276,10 @@ sub _validate_input {
my $type = $c->{type} // 'scalar'; my $type = $c->{type} // 'scalar';
# accept_array (needs to be done before 'trim')
$_[1] = $_[1]->@* == 0 ? undef : $c->{accept_array} eq 'first' ? $_[1][0] : $_[1][ $#{$_[1]} ]
if $c->{accept_array} && ref $_[1] eq 'ARRAY';
# trim (needs to be done before the 'default' test) # trim (needs to be done before the 'default' test)
$_[1] = trim $_[1] =~ s/\r//rg if defined $_[1] && !ref $_[1] && $type eq 'scalar' && (!exists $c->{trim} || $c->{trim}); $_[1] = trim $_[1] =~ s/\r//rg if defined $_[1] && !ref $_[1] && $type eq 'scalar' && (!exists $c->{trim} || $c->{trim});
@ -305,8 +312,8 @@ sub _validate_input {
} }
} elsif ($type eq 'array') { } elsif ($type eq 'array') {
$_[1] = [$_[1]] if $c->{scalar} && !ref $_[1]; $_[1] = [$_[1]] if $c->{accept_scalar} && !ref $_[1];
return { validation => 'type', expected => $c->{scalar} ? 'array or scalar' : 'array', got => lc ref $_[1] || 'scalar' } if ref $_[1] ne 'ARRAY'; return { validation => 'type', expected => $c->{accept_scalar} ? 'array or scalar' : 'array', got => lc ref $_[1] || 'scalar' } if ref $_[1] ne 'ARRAY';
$_[1] = [$_[1]->@*]; # Create a shallow copy to prevent in-place modification. $_[1] = [$_[1]->@*]; # Create a shallow copy to prevent in-place modification.
} elsif ($type eq 'any') { } elsif ($type eq 'any') {
@ -600,10 +607,10 @@ Failure is reported in a similar fashion to I<keys>:
] ]
} }
=item scalar => 0/1 =item accept_scalar => 0/1
Implies C<< type => 'array' >>, this option will also permit the input to be a Implies C<< type => 'array' >>, this option will also permit the input to be a
scalar. In this case, the input is interpreted and returned as an array with scalar. In that case, the input is interpreted and returned as an array with
only one element. This option exists to make it easy to validate multi-value only one element. This option exists to make it easy to validate multi-value
form inputs. For example, consider C<query_decode()> in L<FU::Util>: a form inputs. For example, consider C<query_decode()> in L<FU::Util>: a
parameter in a query string is decoded into an array if it is listed multiple parameter in a query string is decoded into an array if it is listed multiple
@ -613,16 +620,24 @@ times, a scalar if it only occcurs once. So we could either end up with:
# OR: # OR:
{ a => [1, 3], b => 1 } { a => [1, 3], b => 1 }
With the I<scalar> option, we can accept both forms for C<a> and normalize into With the I<accept_scalar> option, we can accept both forms for C<a> and
an array. The following schema definition can validate the above examples: normalize into an array. The following schema definition can validate the above
examples:
{ type => 'hash', { type => 'hash',
keys => { keys => {
a => { type => 'array', scalar => 1 }, a => { type => 'array', accept_scalar => 1 },
b => { } b => { }
} }
} }
=item accept_array => false/'first'/'last'
Implies C<< type => 'scalar' >>. Similar to I<accept_scalar> but normalizes in
the other direction: when the input is an array, only the first or last item is
extracted and the other elements are ignored. If the input is an empty array,
the value is taken to be C<undef>.
=item sort => $option =item sort => $option
Implies C<< type => 'array' >>, sort the array after validating its elements. Implies C<< type => 'array' >>, sort the array after validating its elements.

View file

@ -79,12 +79,20 @@ t { trim => 0 }, " Va\rl id \n ", " Va\rl id \n ";
f {}, ' ', { validation => 'required' }, 'required value missing'; f {}, ' ', { validation => 'required' }, 'required value missing';
t { trim => 0 }, ' ', ' '; t { trim => 0 }, ' ', ' ';
# accept_array
t { default => undef, accept_array => 'first' }, [], undef;
t { default => undef, accept_array => 'first' }, [' x '], 'x';
t { accept_array => 'first' }, [1,2,3], 1;
t { accept_array => 'last' }, [1,2,3], 3;
f { accept_array => 'first' }, [' ', 1], { validation => 'required' }, 'required value missing';
f { accept_array => 'first' }, [], { validation => 'required' }, 'required value missing';
# arrays # arrays
f {}, [], { validation => 'type', expected => 'scalar', got => 'array' }, "invalid type, expected 'scalar' but got 'array'"; f {}, [], { validation => 'type', expected => 'scalar', got => 'array' }, "invalid type, expected 'scalar' but got 'array'";
f { type => 'array' }, 1, { validation => 'type', expected => 'array', got => 'scalar' }, "invalid type, expected 'array' but got 'scalar'"; f { type => 'array' }, 1, { validation => 'type', expected => 'array', got => 'scalar' }, "invalid type, expected 'array' but got 'scalar'";
t { type => 'array' }, [], []; t { type => 'array' }, [], [];
t { type => 'array' }, [undef,1,2,{}], [undef,1,2,{}]; t { type => 'array' }, [undef,1,2,{}], [undef,1,2,{}];
t { type => 'array', scalar => 1 }, 1, [1]; t { type => 'array', accept_scalar => 1 }, 1, [1];
f { type => 'array', elems => {} }, [undef], { validation => 'elems', errors => [{ index => 0, validation => 'required' }] }, "[0]: required value missing"; 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', elems => {} }, [' a '], ['a'];
t { type => 'array', sort => 'str' }, [qw/20 100 3/], [qw/100 20 3/]; t { type => 'array', sort => 'str' }, [qw/20 100 3/], [qw/100 20 3/];