Validate: Rework API, ->validate() now throws error instead of result object

This is a slight simplification and removes the need to pass around
partially normalized data. I've never found a use for the unsafe_data()
method.
This commit is contained in:
Yorhel 2025-03-05 15:32:01 +01:00
parent 7839e7df78
commit cbebc3a21e
3 changed files with 295 additions and 347 deletions

5
FU.pm
View file

@ -639,8 +639,8 @@ sub _getfield($data, @a) {
return $data->{$a[0]} if @a == 1 && !ref $a[0];
require FU::Validate;
my $schema = FU::Validate->compile(@a > 1 ? { keys => {@a} } : $a[0]);
my $res = $schema->validate($data);
fu->error(400, "Input validation failed") if !$res; # TODO: More detailed error message
my $res = eval { $schema->validate($data) };
fu->error(400, "Input validation failed") if $@; # TODO: More detailed error message
return @a == 2 ? $res->data->{$a[0]} : $res->data;
}
@ -659,7 +659,6 @@ sub formdata {
if (fu->header('content-type')||'') ne 'application/x-www-form-urlencoded';
FU::Util::query_decode($FU::REQ->{data});
} || fu->error(400, $@);
# TODO: Accept schema validation thing.
_getfield $FU::REQ->{formdata}, @_;
}

View file

@ -102,7 +102,7 @@ sub _compile($schema, $validations, $rec) {
my @keys = keys $schema->{keys}->%* if $schema->{keys};
for my($name, $val) (%$schema) {
if($builtin{$name}) {
if ($builtin{$name}) {
$top{$name} = $schema->{$name};
next;
}
@ -125,7 +125,7 @@ sub _compile($schema, $validations, $rec) {
# Inherit some builtin options from validations
for my $t (@val) {
if($top{type} && $t->{schema}{type} && $top{type} ne $t->{schema}{type}) {
if ($top{type} && $t->{schema}{type} && $top{type} ne $t->{schema}{type}) {
confess "Incompatible types, the schema specifies '$top{type}' but validation '$t->{name}' requires '$t->{schema}{type}'" if $schema->{type};
confess "Incompatible types, '$t->[0]' requires '$t->{schema}{type}', but another validation requires '$top{type}'";
}
@ -164,7 +164,7 @@ sub compile($pkg, $schema, $validations={}) {
delete $c->{schema}{default} if ref $c->{schema}{default} eq 'SCALAR' && ${$c->{schema}{default}} eq 'required';
if(exists $c->{schema}{sort}) {
if (exists $c->{schema}{sort}) {
my $s = $c->{schema}{sort};
$c->{schema}{sort} =
ref $s eq 'CODE' ? $s
@ -178,186 +178,169 @@ sub compile($pkg, $schema, $validations={}) {
}
sub _validate_rec($c, $input) {
sub _validate_rec {
my $c = $_[0];
# hash keys
if($c->{schema}{keys}) {
if ($c->{schema}{keys}) {
my @err;
for my ($k, $s) ($c->{schema}{keys}->%*) {
if(!exists $input->{$k}) {
if (!exists $_[1]{$k}) {
next if $s->{schema}{missing} eq 'ignore';
return [$input, { validation => 'missing', key => $k }] if $s->{schema}{missing} eq 'reject';
$input->{$k} = ref $s->{schema}{default} eq 'CODE' ? $s->{schema}{default}->() : $s->{schema}{default} // undef;
return { validation => 'missing', key => $k } if $s->{schema}{missing} eq 'reject';
$_[1]{$k} = ref $s->{schema}{default} eq 'CODE' ? $s->{schema}{default}->() : $s->{schema}{default} // undef;
next if exists $s->{schema}{default};
}
my $r = _validate($s, $input->{$k});
$input->{$k} = $r->[0];
if($r->[1]) {
$r->[1]{key} = $k;
push @err, $r->[1];
my $r = _validate($s, $_[1]{$k});
if ($r) {
$r->{key} = $k;
push @err, $r;
}
}
return [$input, { validation => 'keys', errors => \@err }] if @err;
return { validation => 'keys', errors => \@err } if @err;
}
# array values
if($c->{schema}{values}) {
if ($c->{schema}{values}) {
my @err;
for my $i (0..$#$input) {
my $r = _validate($c->{schema}{values}, $input->[$i]);
$input->[$i] = $r->[0];
if($r->[1]) {
$r->[1]{index} = $i;
push @err, $r->[1];
for my $i (0..$#{$_[1]}) {
my $r = _validate($c->{schema}{values}, $_[1][$i]);
if ($r) {
$r->{index} = $i;
push @err, $r;
}
}
return [$input, { validation => 'values', errors => \@err }] if @err;
return { validation => 'values', errors => \@err } if @err;
}
# validations
for ($c->{validations}->@*) {
my $r = _validate_rec($_, $input);
$input = $r->[0];
return [$input, {
my $r = _validate_rec($_, $_[1]);
return {
# If the error was a custom 'func' object, then make that the primary cause.
# This makes it possible for validations to provide their own error objects.
$r->[1]{validation} eq 'func' && (!exists $r->[1]{result} || keys $r->[1]->%* > 2) ? $r->[1]->%* : (error => $r->[1]),
$r->{validation} eq 'func' && (!exists $r->{result} || keys $r->%* > 2) ? $r->%* : (error => $r),
validation => $_->{name},
}] if $r->[1];
} if $r;
}
# func
if($c->{schema}{func}) {
my $r = $c->{schema}{func}->($input);
return [$input, { %$r, validation => 'func' }] if ref $r eq 'HASH';
return [$input, { validation => 'func', result => $r }] if !$r;
if ($c->{schema}{func}) {
my $r = $c->{schema}{func}->($_[1]);
return { %$r, validation => 'func' } if ref $r eq 'HASH';
return { validation => 'func', result => $r } if !$r;
}
return [$input]
}
sub _validate_array($c, $input) {
return [$input] if $c->{schema}{type} ne 'array';
sub _validate_array {
my $c = $_[0];
return if $c->{schema}{type} ne 'array';
$input = [sort { $c->{schema}{sort}->($a, $b) } @$input ] if $c->{schema}{sort};
$_[1] = [sort { $c->{schema}{sort}->($a, $b) } $_[1]->@* ] if $c->{schema}{sort};
# Key-based uniqueness
if($c->{schema}{unique} && ref $c->{schema}{unique} eq 'CODE') {
if ($c->{schema}{unique} && ref $c->{schema}{unique} eq 'CODE') {
my %h;
for my $i (0..$#$input) {
my $k = $c->{schema}{unique}->($input->[$i]);
return [$input, { validation => 'unique', index_a => $h{$k}, value_a => $input->[$h{$k}], index_b => $i, value_b => $input->[$i], key => $k }] if exists $h{$k};
for my $i (0..$#{$_[1]}) {
my $k = $c->{schema}{unique}->($_[1][$i]);
return { validation => 'unique', index_a => $h{$k}, value_a => $_[1][$h{$k}], index_b => $i, value_b => $_[1][$i], key => $k } if exists $h{$k};
$h{$k} = $i;
}
# Comparison-based uniqueness
} elsif($c->{schema}{unique}) {
for my $i (0..$#$input-1) {
return [$input, { validation => 'unique', index_a => $i, value_a => $input->[$i], index_b => $i+1, value_b => $input->[$i+1] }]
if $c->{schema}{sort}->($input->[$i], $input->[$i+1]) == 0
} elsif ($c->{schema}{unique}) {
for my $i (0..$#{$_[1]}-1) {
return { validation => 'unique', index_a => $i, value_a => $_[1][$i], index_b => $i+1, value_b => $_[1][$i+1] }
if $c->{schema}{sort}->($_[1][$i], $_[1][$i+1]) == 0
}
}
return [$input]
}
sub _validate_input($c, $input) {
sub _validate_input {
my $c = $_[0];
# rmwhitespace (needs to be done before the 'default' test)
if(defined $input && !ref $input && $c->{schema}{type} eq 'scalar' && $c->{schema}{rmwhitespace}) {
$input =~ s/\r//g;
$input =~ s/^\s*//;
$input =~ s/\s*$//;
if (defined $_[1] && !ref $_[1] && $c->{schema}{type} eq 'scalar' && $c->{schema}{rmwhitespace}) {
$_[1] =~ s/\r//g;
$_[1] =~ s/^\s*//;
$_[1] =~ s/\s*$//;
}
# default
if(!defined $input || (!ref $input && $input eq '')) {
return [ref $c->{schema}{default} eq 'CODE' ? $c->{schema}{default}->($input) : $c->{schema}{default}] if exists $c->{schema}{default};
return [$input, { validation => 'required' }];
if (!defined $_[1] || (!ref $_[1] && $_[1] eq '')) {
if (exists $c->{schema}{default}) {
$_[1] = ref $c->{schema}{default} eq 'CODE' ? $c->{schema}{default}->($_[1]) : $c->{schema}{default};
return;
}
return { validation => 'required' };
}
if($c->{schema}{type} eq 'scalar') {
return [$input, { validation => 'type', expected => 'scalar', got => lc ref $input }] if ref $input;
if ($c->{schema}{type} eq 'scalar') {
return { validation => 'type', expected => 'scalar', got => lc ref $_[1] } if ref $_[1];
} elsif($c->{schema}{type} eq 'hash') {
return [$input, { validation => 'type', expected => 'hash', got => lc ref $input || 'scalar' }] if ref $input ne 'HASH';
} elsif ($c->{schema}{type} eq 'hash') {
return { validation => 'type', expected => 'hash', got => lc ref $_[1] || 'scalar' } if ref $_[1] ne 'HASH';
# 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->{schema}{unknown} eq 'remove') {
$input = { map +($_, $input->{$_}), grep $c->{known_keys}{$_}, keys %$input };
} elsif($c->{schema}{unknown} eq 'reject') {
my @err = grep !$c->{known_keys}{$_}, keys %$input;
return [$input, { validation => 'unknown', keys => \@err, expected => [ sort keys %{$c->{known_keys}} ] }] if @err;
$input = { %$input };
if ($c->{schema}{unknown} eq 'remove') {
$_[1] = { map +($_, $_[1]{$_}), grep $c->{known_keys}{$_}, keys $_[1]->%* };
} elsif ($c->{schema}{unknown} eq 'reject') {
my @err = grep !$c->{known_keys}{$_}, keys $_[1]->%*;
return { validation => 'unknown', keys => \@err, expected => [ sort keys %{$c->{known_keys}} ] } if @err;
$_[1] = { $_[1]->%* };
} else {
$input = { %$input };
$_[1] = { $_[1]->%* };
}
} elsif($c->{schema}{type} eq 'array') {
$input = [$input] if $c->{schema}{scalar} && !ref $input;
return [$input, { validation => 'type', expected => $c->{schema}{scalar} ? 'array or scalar' : 'array', got => lc ref $input || 'scalar' }] if ref $input ne 'ARRAY';
$input = [@$input]; # Create a shallow copy to prevent in-place modification.
} elsif ($c->{schema}{type} eq 'array') {
$_[1] = [$_[1]] if $c->{schema}{scalar} && !ref $_[1];
return { validation => 'type', expected => $c->{schema}{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.
} elsif($c->{schema}{type} eq 'any') {
} elsif ($c->{schema}{type} eq 'any') {
# No need to do anything here.
} else {
confess "Unknown type '$c->{schema}{type}'"; # Already checked in compile(), but be extra safe
}
my $r = _validate_rec($c, $input);
return $r if $r->[1];
$input = $r->[0];
_validate_array($c, $input);
&_validate_rec || &_validate_array;
}
sub _validate($c, $input) {
my $r = _validate_input($c, $input);
return $r if !$r->[1] || !exists $c->{schema}{onerror};
[ ref $c->{schema}{onerror} eq 'CODE' ? $c->{schema}{onerror}->(bless $r, 'FU::Validate::Result') : $c->{schema}{onerror} ]
sub _validate {
my $c = $_[0];
my $r = &_validate_input;
($r, $_[1]) = (undef, ref $c->{schema}{onerror} eq 'CODE' ? $c->{schema}{onerror}->($_[0], bless $r, 'FU::Validate::err') : $c->{schema}{onerror})
if $r && exists $c->{schema}{onerror};
$r
}
sub validate($c, $input) {
bless _validate($c, $input), 'FU::Validate::Result';
my $r = _validate($c, $input);
return $input if !$r;
die bless $r, 'FU::Validate::err';
$input
}
package FU::Validate::Result;
package FU::Validate::err;
use v5.36;
use Carp 'confess';
# A result object contains: [$data, $error]
use overload '""' => sub {
# TODO: Better error message
require Data::Dumper;
Data::Dumper->new([{$_[0]->%*}])->Terse(1)->Pair(':')->Indent(0)->Sortkeys(1)->Dump."\n";
};
# In boolean context, returns whether the validation succeeded.
use overload bool => sub { !$_[0][1] };
# Returns the validation errors, or undef if validation succeeded
sub err { $_[0][1] }
# Returns the validated and normalized input, dies if validation didn't succeed.
sub data {
if($_[0][1]) {
require Data::Dumper;
my $s = Data::Dumper->new([$_[0][1]])->Terse(1)->Pair(':')->Indent(0)->Sortkeys(1)->Dump;
confess "Validation failed: $s";
}
$_[0][0]
}
# Same as 'data', but returns partially validated and normalized data if validation failed.
sub unsafe_data { $_[0][0] }
# TODO: Human-readable error message formatting
1;
__END__
@ -402,67 +385,22 @@ follows:
C<$schema> is the schema that describes the data to be validated (see L</SCHEMA
DEFINITION> below) and C<$validations> is an optional hashref containing
L<custom validations|/Custom validations> that C<$schema> can refer to.
L<custom validations|/Custom validations> that C<$schema> can refer to. An
error is thrown if the C<$validations> or C<$schema> are invalid.
To validate input, run:
my $result = $validator->validate($input);
my $validated_input = $validator->validate($input);
C<$input> is the data to be validated, and the C<$result> object is L<described
below|/Result object>.
C<validate()> returns a validated and (depending on the schema) normalized copy
of C<$input>. Great care is taken that C<$input> is not being modified
in-place, even if data normalization is being performed.
Both C<compile()> and C<validate()> may throw an error if the C<$validations>
or C<$schema> are invalid. Errors in the C<$input> should never cause an error
to be thrown, since these are always reported in the C<$result> object.
This module takes great care that C<$input> is not being modified in place,
even if data normalization is being performed. The normalized data can be read
from the C<$result> object.
=head2 Result object
The C<$result> object returned by C<validate()> overloads boolean context, so
you can check if the validation succeeded with a simple if statement:
my $result = $validator->validate(..);
if($result) {
# Success!
my $data = $result->data;
} else {
# Input failed to validate...
my $error = $result->err;
}
In addition, the result object implements the following methods:
=over
=item data()
Returns the validated and normalized data. This method throws an error if
validation failed, so if you're lazy and don't want to bother too much with
proper error reporting, you can safely I<validate-and-die> in a single step:
my $validated_data = $v->validate(..)->data;
(Note regarding reference semantics: The returned data will usually be a
(possibly modified) copy of C<$input>, but may in some cases still have nested
references to data in C<$input> - so if you are working with nested hashrefs,
arrayrefs or other objects and are going to make modifications to the values
embedded within them, these changes may or may not also affect the values in
the original C<$input>. Make a deep copy of the data if you're concerned about
this).
=item err()
Returns I<undef> if validation succeeded, an error object otherwise.
An error object is a hashref containing at least one key: I<validation>, which
indicates the name of the validation that failed. Additional keys with more
detailed information may be present, depending on the validation. These are
documented in L</SCHEMA DEFINITION> below.
=back
An error is thrown if the input does not validate. The error object is a
C<FU::Validate::err>-blessed hashref containing at least one key:
I<validation>, which indicates the name of the validation that failed.
Additional keys with more detailed information may be present, depending on the
validation. These are documented in L</SCHEMA DEFINITION> below.
=head1 SCHEMA DEFINITION
@ -519,9 +457,9 @@ Instead of reporting an error, return C<$val> if this input fails validation
for whatever reason. Setting this option in the top-level schema ensures that
the validation will always succeed regardless of the input.
If C<$val> is a CODE reference, the subroutine is called with the result object
for this validation as its first argument. The return value of the subroutine
is then returned for this validation.
If C<$val> is a CODE reference, the subroutine is called with the (partially
normalized) input as first argument and error object as second argument. The
return value of the subroutine is then returned for this validation.
=item rmwhitespace => 0/1
@ -856,11 +794,11 @@ Here's a simple example that defines and uses a custom validation named
I<stringbool>, which accepts either the string I<true> or I<false>.
my $validations = {
stringbool => { enum => ['true', 'false'] }
stringbool => { enum => ['true', 'false'] }
};
my $schema = { stringbool => 1 };
my $result = FU::Validate->compile($schema, $validations)->validate('true');
# $result->data() eq 'true'
# $result eq 'true'
A custom validation can also be defined as a subroutine, in which case it can
accept options. Here is an example of a I<prefix> custom validation, which
@ -868,9 +806,9 @@ requires that the string starts with the given prefix. The subroutine returns a
schema that contains the I<func> built-in option to do the actual validation.
my $validations = {
prefix => sub($prefix) {
return { func => sub { $_[0] =~ /^\Q$prefix/ } }
}
prefix => sub($prefix) {
return { func => sub { $_[0] =~ /^\Q$prefix/ } }
}
};
my $schema = { prefix => 'Hello, ' };
my $result = FU::Validate->compile($schema, $validations)->validate('Hello, World!');
@ -891,10 +829,10 @@ mixes validations of different types. For example, the following throws an
error:
FU::Validate->compile({
# top-level schema says we expect a hash
type => 'hash',
# but the 'int' validation implies that the type is a scalar
int => 1
# top-level schema says we expect a hash
type => 'hash',
# but the 'int' validation implies that the type is a scalar
int => 1
});
The I<keys>, I<values> and C<func> built-in options are validated separately

View file

@ -14,7 +14,7 @@ my %validations = (
setundef => { func => sub { $_[0] = undef; 1 } },
defaultsub1 => { default => sub { 2 } },
defaultsub2 => { default => sub { defined $_[0] } },
onerrorsub => { onerror => sub { ref $_[0] } },
onerrorsub => { onerror => sub { ref $_[1] } },
collapsews => { rmwhitespace => 0, func => sub { $_[0] =~ s/\s+/ /g; 1 } },
neverfails => { onerror => 'err' },
revnum => { type => 'array', sort => sub($x,$y) { $y <=> $x } },
@ -31,8 +31,7 @@ my %validations = (
);
sub t {
my($schema, $input, $output, $error) = @_;
sub t($schema, $input, $output) {
my $line = (caller)[2];
my $schema_copy = dclone([$schema])->[0];
@ -40,203 +39,215 @@ sub t {
my $res = FU::Validate->compile($schema, \%validations)->validate($input);
#diag explain FU::Validate->compile($schema, \%validations) if $line == 139;
is !$error, !!$res, "boolean context $line";
is_deeply $schema, $schema_copy, "schema modification $line";
is_deeply $input, $input_copy, "input modification $line";
is_deeply $res->unsafe_data(), $output, "unsafe_data $line";
is_deeply $res->data(), $output, "data ok $line" if !$error;
ok !eval { $res->data; 1}, "data err $line" if $error;
is_deeply $res->err(), $error, "err $line";
is_deeply $res, $output, "data ok $line";
}
sub f($schema, $input, $error) {
my $line = (caller)[2];
my $schema_copy = dclone([$schema])->[0];
my $input_copy = dclone([$input])->[0];
ok !eval { FU::Validate->compile($schema, \%validations)->validate($input); 1 }, "eval $line";
is_deeply $schema, $schema_copy, "schema modification $line";
is_deeply $input, $input_copy, "input modification $line";
is_deeply { $@->%* }, $error, "err $line";
}
# default
t {}, 0, 0, undef;
t {}, '', '', { validation => 'required' };
t {}, undef, undef, { validation => 'required' };
t { default => undef }, undef, undef, undef;
t { default => undef }, '', undef, undef;
t { defaultsub1 => 1 }, undef, 2, undef;
t { defaultsub2 => 1 }, undef, '', undef;
t { defaultsub2 => 1 }, '', 1, undef;
t { onerrorsub => 1 }, undef, 'FU::Validate::Result', undef;
t {}, 0, 0;
f {}, '', { validation => 'required' };
f {}, undef, { validation => 'required' };
t { default => undef }, undef, undef;
t { default => undef }, '', undef;
t { defaultsub1 => 1 }, undef, 2;
t { defaultsub2 => 1 }, undef, '';
t { defaultsub2 => 1 }, '', 1;
t { onerrorsub => 1 }, undef, 'FU::Validate::err';
# rmwhitespace
t {}, " Va\rl id \n ", 'Val id', undef;
t { rmwhitespace => 0 }, " Va\rl id \n ", " Va\rl id \n ", undef;
t {}, ' ', '', { validation => 'required' };
t { rmwhitespace => 0 }, ' ', ' ', undef;
t {}, " Va\rl id \n ", 'Val id';
t { rmwhitespace => 0 }, " Va\rl id \n ", " Va\rl id \n ";
f {}, ' ', { validation => 'required' };
t { rmwhitespace => 0 }, ' ', ' ';
# arrays
t {}, [], [], { validation => 'type', expected => 'scalar', got => 'array' };
t { type => 'array' }, 1, 1, { validation => 'type', expected => 'array', got => 'scalar' };
t { type => 'array' }, [], [], undef;
t { type => 'array' }, [undef,1,2,{}], [undef,1,2,{}], undef;
t { type => 'array', scalar => 1 }, 1, [1], undef;
t { type => 'array', values => {} }, [undef], [undef], { validation => 'values', errors => [{ index => 0, validation => 'required' }] };
t { type => 'array', values => {} }, [' a '], ['a'], undef;
t { type => 'array', sort => 'str' }, [qw/20 100 3/], [qw/100 20 3/], undef;
t { type => 'array', sort => 'num' }, [qw/20 100 3/], [qw/3 20 100/], undef;
t { revnum => 1 }, [qw/20 100 3/], [qw/100 20 3/], undef;
t { type => 'array', sort => 'num', unique => 1 }, [qw/3 2 1/], [qw/1 2 3/], undef;
t { type => 'array', sort => 'num', unique => 1 }, [qw/3 2 3/], [qw/2 3 3/], { validation => 'unique', index_a => 1, value_a => 3, index_b => 2, value_b => 3 };
t { type => 'array', unique => 1 }, [qw/3 1 2/], [qw/3 1 2/], undef;
t { type => 'array', unique => 1 }, [qw/3 1 3/], [qw/3 1 3/], { validation => 'unique', index_a => 0, value_a => 3, index_b => 2, value_b => 3, key => 3 };
t { uniquelength => 1 }, [[],[1],[1,2]], [[],[1],[1,2]], undef;
t { uniquelength => 1 }, [[],[1],[2]], [[],[1],[2]], { validation => 'unique', index_a => 1, value_a => [1], index_b => 2, value_b => [2], key => 1 };
t { type => 'array', setundef => 1 }, [], undef, undef;
t { type => 'array', values => { type => 'any', setundef => 1 } }, [[]], [undef], undef;
f {}, [], { validation => 'type', expected => 'scalar', got => 'array' };
f { type => 'array' }, 1, { validation => 'type', expected => 'array', got => 'scalar' };
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' }] };
t { type => 'array', values => {} }, [' 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/];
t { type => 'array', sort => 'num', unique => 1 }, [qw/3 2 1/], [qw/1 2 3/];
f { type => 'array', sort => 'num', unique => 1 }, [qw/3 2 3/], { validation => 'unique', index_a => 1, value_a => 3, index_b => 2, value_b => 3 };
t { type => 'array', unique => 1 }, [qw/3 1 2/], [qw/3 1 2/];
f { type => 'array', unique => 1 }, [qw/3 1 3/], { validation => 'unique', index_a => 0, value_a => 3, index_b => 2, value_b => 3, key => 3 };
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 };
t { type => 'array', setundef => 1 }, [], undef;
t { type => 'array', values => { type => 'any', setundef => 1 } }, [[]], [undef];
# hashes
t { type => 'hash' }, [], [], { validation => 'type', expected => 'hash', got => 'array' };
t { type => 'hash' }, 'a', 'a', { validation => 'type', expected => 'hash', got => 'scalar' };
t { type => 'hash' }, {a=>[],b=>undef,c=>{}}, {}, undef;
t { type => 'hash', keys => { a=>{} } }, {}, {a=>undef}, { validation => 'keys', errors => [{ key => 'a', validation => 'required' }] }; # XXX: the key doesn't necessarily have to be created
t { type => 'hash', keys => { a=>{missing=>'ignore'} } }, {}, {}, undef;
t { type => 'hash', keys => { a=>{default=>undef} } }, {}, {a=>undef}, undef;
t { type => 'hash', keys => { a=>{missing=>'create',default=>undef} } }, {}, {a=>undef}, undef;
t { type => 'hash', keys => { a=>{missing=>'reject'} } }, {}, {}, {key => 'a', validation => 'missing'};
f { type => 'hash' }, [], { validation => 'type', expected => 'hash', got => 'array' };
f { type => 'hash' }, 'a', { validation => 'type', expected => 'hash', got => 'scalar' };
t { type => 'hash' }, {a=>[],b=>undef,c=>{}}, {};
f { type => 'hash', keys => { a=>{} } }, {}, { validation => 'keys', errors => [{ key => 'a', validation => 'required' }] };
t { type => 'hash', keys => { a=>{missing=>'ignore'} } }, {}, {};
t { type => 'hash', keys => { a=>{default=>undef} } }, {}, {a=>undef};
t { type => 'hash', keys => { a=>{missing=>'create',default=>undef} } }, {}, {a=>undef};
f { type => 'hash', keys => { a=>{missing=>'reject'} } }, {}, {key => 'a', validation => 'missing'};
t { type => 'hash', keys => { a=>{} } }, {a=>' a '}, {a=>'a'}, undef; # Test against in-place modification
t { type => 'hash', keys => { a=>{} }, unknown => 'remove' }, { a=>1,b=>1 }, { a=>1 }, undef;
t { type => 'hash', keys => { a=>{} }, unknown => 'reject' }, { a=>1,b=>1 }, { a=>1,b=>1 }, { validation => 'unknown', keys => ['b'], expected => ['a'] };
t { type => 'hash', keys => { a=>{} }, unknown => 'pass' }, { a=>1,b=>1 }, { a=>1,b=>1 }, undef;
t { type => 'hash', setundef => 1 }, {}, undef, undef;
t { type => 'hash', unknown => 'reject', keys => { a=>{ type => 'any', setundef => 1}} }, {a=>[]}, {a=>undef}, undef;
t { type => 'hash', keys => { a=>{} } }, {a=>' a '}, {a=>'a'}; # Test against in-place modification
t { type => 'hash', keys => { a=>{} }, unknown => 'remove' }, { a=>1,b=>1 }, { a=>1 };
f { type => 'hash', keys => { a=>{} }, unknown => 'reject' }, { a=>1,b=>1 }, { validation => 'unknown', keys => ['b'], expected => ['a'] };
t { type => 'hash', keys => { a=>{} }, unknown => 'pass' }, { a=>1,b=>1 }, { a=>1,b=>1 };
t { type => 'hash', setundef => 1 }, {}, undef;
t { type => 'hash', unknown => 'reject', keys => { a=>{ type => 'any', setundef => 1}} }, {a=>[]}, {a=>undef};
# default validations
t { minlength => 3 }, 'ab', 'ab', { validation => 'minlength', expected => 3, got => 2 };
t { minlength => 3 }, 'abc', 'abc', undef;
t { maxlength => 3 }, 'abcd', 'abcd', { validation => 'maxlength', expected => 3, got => 4 };
t { maxlength => 3 }, 'abc', 'abc', undef;
t { minlength => 3, maxlength => 3 }, 'abc', 'abc', undef;
t { length => 3 }, 'ab', 'ab', { validation => 'length', expected => 3, got => 2 };
t { length => 3 }, 'abcd', 'abcd', { validation => 'length', expected => 3, got => 4 };
t { length => 3 }, 'abc', 'abc', undef;
t { length => [1,3] }, 'abc', 'abc', undef;
t { length => [1,3] }, 'abcd', 'abcd', { validation => 'length', expected => [1,3], got => 4 };;
t { type => 'array', length => 0 }, [], [], undef;
t { type => 'array', length => 1 }, [1,2], [1,2], { validation => 'length', expected => 1, got => 2 };
t { type => 'hash', length => 0 }, {}, {}, undef;
t { type => 'hash', length => 1, unknown => 'pass' }, {qw/1 a 2 b/}, {qw/1 a 2 b/}, { validation => 'length', expected => 1, got => 2 };
t { type => 'hash', length => 1, keys => {a => {missing=>'ignore'}, b => {missing=>'ignore'}} }, {a=>1}, {a=>1}, undef;
t { regex => '^a' }, 'abc', 'abc', undef; # XXX: Can't use qr// here because t() does dclone(). The 'hex' test covers that case anyway.
t { regex => '^a' }, 'cba', 'cba', { validation => 'regex', regex => '^a', got => 'cba' };
t { enum => [1,2] }, 1, 1, undef;
t { enum => [1,2] }, 2, 2, undef;
t { enum => [1,2] }, 3, 3, { validation => 'enum', expected => [1,2], got => 3 };
t { enum => 1 }, 1, 1, undef;
t { enum => 1 }, 2, 2, { validation => 'enum', expected => [1], got => 2 };
t { enum => {a=>1,b=>2} }, 'a', 'a', undef;
t { enum => {a=>1,b=>2} }, 'c', 'c', { validation => 'enum', expected => ['a','b'], got => 'c' };
t { anybool => 1 }, 1, true, undef;
t { anybool => 1 }, undef, false, undef;
t { anybool => 1 }, '', false, undef;
t { anybool => 1 }, {}, true, undef;
t { anybool => 1 }, [], true, undef;
t { anybool => 1 }, bless({}, 'test'), true, undef;
t { bool => 1 }, 1, 1, { validation => 'bool' };
t { bool => 1 }, \1, true, undef;
f { minlength => 3 }, 'ab', { validation => 'minlength', expected => 3, got => 2 };
t { minlength => 3 }, 'abc', 'abc';
f { maxlength => 3 }, 'abcd', { validation => 'maxlength', expected => 3, got => 4 };
t { maxlength => 3 }, 'abc', 'abc';
t { minlength => 3, maxlength => 3 }, 'abc', 'abc';
f { length => 3 }, 'ab', { validation => 'length', expected => 3, got => 2 };
f { length => 3 }, 'abcd', { validation => 'length', expected => 3, got => 4 };
t { length => 3 }, 'abc', 'abc';
t { length => [1,3] }, 'abc', 'abc';
f { length => [1,3] }, 'abcd', { validation => 'length', expected => [1,3], got => 4 };;
t { type => 'array', length => 0 }, [], [];
f { type => 'array', length => 1 }, [1,2], { validation => 'length', expected => 1, got => 2 };
t { type => 'hash', length => 0 }, {}, {};
f { type => 'hash', length => 1, unknown => 'pass' }, {qw/1 a 2 b/}, { validation => 'length', expected => 1, got => 2 };
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' };
t { enum => [1,2] }, 1, 1;
t { enum => [1,2] }, 2, 2;
f { enum => [1,2] }, 3, { validation => 'enum', expected => [1,2], got => 3 };
t { enum => 1 }, 1, 1;
f { enum => 1 }, 2, { validation => 'enum', expected => [1], got => 2 };
t { enum => {a=>1,b=>2} }, 'a', 'a';
f { enum => {a=>1,b=>2} }, 'c', { validation => 'enum', expected => ['a','b'], got => 'c' };
t { anybool => 1 }, 1, true;
t { anybool => 1 }, undef, false;
t { anybool => 1 }, '', false;
t { anybool => 1 }, {}, true;
t { anybool => 1 }, [], true;
t { anybool => 1 }, bless({}, 'test'), true;
f { bool => 1 }, 1, { validation => 'bool' };
t { bool => 1 }, \1, true;
my($true, $false) = (1,0);
t { bool => 1 }, bless(\$true, 'boolean'), true, undef;
t { bool => 1 }, bless(\$false, 'boolean'), false, undef;
t { bool => 1 }, bless(\$true, 'test'), bless(\$true, 'test'), { validation => 'bool' };
t { ascii => 1 }, 'ab c', 'ab c', undef;
t { ascii => 1 }, "a\nb", "a\nb", { validation => 'ascii', got => "a\nb" };
t { bool => 1 }, bless(\$true, 'boolean'), true;
t { bool => 1 }, bless(\$false, 'boolean'), false;
f { bool => 1 }, bless(\$true, 'test'), { validation => 'bool' };
t { ascii => 1 }, 'ab c', 'ab c';
f { ascii => 1 }, "a\nb", { validation => 'ascii', got => "a\nb" };
# custom validations
t { hex => 1 }, 'DeadBeef', 'DeadBeef', undef;
t { hex => 1 }, 'x', 'x', { validation => 'hex', error => { validation => 'regex', regex => "$validations{hex}{regex}", got => 'x' } };
t { prefix => 'a' }, 'abc', 'abc', undef;
t { prefix => 'a' }, 'cba', 'cba', { validation => 'prefix', error => { validation => 'func', result => '' } };
t { mybool => 1 }, 'abc', 1, undef;
t { mybool => 1 }, undef, 0, undef;
t { mybool => 1 }, '', 0, undef;
t { collapsews => 1 }, " \t\n ", ' ', undef;
t { collapsews => 1 }, ' x ', ' x ', undef;
t { collapsews => 1, rmwhitespace => 1 }, ' x ', 'x', undef;
t { person => 1 }, 1, 1, { validation => 'type', expected => 'hash', got => 'scalar' };
t { person => 1, default => 1 }, undef, 1, undef;
t { person => 1 }, { sex => 1 }, { sex => 1, name => undef }, { validation => 'person', error => { validation => 'keys', errors => [{ key => 'name', validation => 'required' }] } };
t { person => 1 }, { sex => undef, name => 'y' }, { sex => 1, name => 'y' }, undef;
t { person => 1, keys => {age => {default => \'required'}} }, {name => 'x', sex => 'y'}, { name => 'x', sex => 'y', age => undef }, { validation => 'keys', errors => [{ key => 'age', validation => 'required' }] };
t { person => 1, keys => {extra => {}} }, {name => 'x', sex => 'y', extra => 1}, { name => 'x', sex => 'y', extra => 1 }, undef;
t { person => 1, keys => {extra => {}} }, {name => 'x', sex => 'y', extra => ''}, { name => 'x', sex => 'y', extra => '' }, { validation => 'keys', errors => [{ key => 'extra', validation => 'required' }] };
t { person => 1 }, {name => 'x', sex => 'y', extra => 1}, {name => 'x', sex => 'y', extra => 1}, undef;
t { person => 1, unknown => 'remove' }, {name => 'x', sex => 'y', extra => 1}, {name => 'x', sex => 'y'}, undef;
t { neverfails => 1, int => 1 }, undef, 'err', undef;
t { neverfails => 1, int => 1 }, 'x', 'err', undef;
t { neverfails => 1, int => 1, onerror => undef }, 'x', undef, undef; # XXX: no way to 'unset' an inherited onerror clause, hmm.
t { hex => 1 }, 'DeadBeef', 'DeadBeef';
f { hex => 1 }, 'x', { validation => 'hex', error => { validation => 'regex', regex => "$validations{hex}{regex}", got => 'x' } };
t { prefix => 'a' }, 'abc', 'abc';
f { prefix => 'a' }, 'cba', { validation => 'prefix', error => { validation => 'func', result => '' } };
t { mybool => 1 }, 'abc', 1;
t { mybool => 1 }, undef, 0;
t { mybool => 1 }, '', 0;
t { collapsews => 1 }, " \t\n ", ' ';
t { collapsews => 1 }, ' x ', ' x ';
t { collapsews => 1, rmwhitespace => 1 }, ' x ', 'x';
f { person => 1 }, 1, { validation => 'type', expected => 'hash', got => 'scalar' };
t { person => 1, default => 1 }, undef, 1;
f { person => 1 }, { sex => 1 }, { validation => 'person', error => { validation => 'keys', errors => [{ key => 'name', validation => 'required' }] } };
t { person => 1 }, { sex => undef, name => 'y' }, { sex => 1, name => 'y' };
f { person => 1, keys => {age => {default => \'required'}} }, {name => 'x', sex => 'y'}, { validation => 'keys', errors => [{ key => 'age', validation => 'required' }] };
t { person => 1, keys => {extra => {}} }, {name => 'x', sex => 'y', extra => 1}, { name => 'x', sex => 'y', extra => 1 };
f { person => 1, keys => {extra => {}} }, {name => 'x', sex => 'y', extra => ''}, { validation => 'keys', errors => [{ key => 'extra', validation => 'required' }] };
t { person => 1 }, {name => 'x', sex => 'y', extra => 1}, {name => 'x', sex => 'y', extra => 1};
t { person => 1, unknown => 'remove' }, {name => 'x', sex => 'y', extra => 1}, {name => 'x', sex => 'y'};
t { neverfails => 1, int => 1 }, undef, 'err';
t { neverfails => 1, int => 1 }, 'x', 'err';
t { neverfails => 1, int => 1, onerror => undef }, 'x', undef; # XXX: no way to 'unset' an inherited onerror clause, hmm.
# numbers
sub nerr { +{ validation => 'num', got => $_[0] } }
t { num => 1 }, 0, 0, undef;
t { num => 1 }, '-', '-', nerr '-';
t { num => 1 }, '00', '00', nerr '00';
t { num => 1 }, '1', '1', undef;
t { num => 1 }, '1.1.', '1.1.', nerr '1.1.';
t { num => 1 }, '1.-1', '1.-1', nerr '1.-1';
t { num => 1 }, '.1', '.1', nerr '.1';
t { num => 1 }, '0.1e5', '0.1e5', undef;
t { num => 1 }, '0.1e+5', '0.1e+5', undef;
t { num => 1 }, '0.1e5.1', '0.1e5.1', nerr '0.1e5.1';
t { int => 1 }, 0, 0, undef;
t { int => 1 }, -123, -123, undef;
t { int => 1 }, -123.1, -123.1, { validation => 'int', got => -123.1 };
t { uint => 1 }, 0, 0, undef;
t { uint => 1 }, 123, 123, undef;
t { uint => 1 }, -123, -123, { validation => 'uint', got => -123 };
t { min => 1 }, 1, 1, undef;
t { min => 1 }, 0.9, 0.9, { validation => 'min', expected => 1, got => 0.9 };
t { min => 1 }, 'a', 'a', { validation => 'min', error => nerr 'a' };
t { max => 1 }, 1, 1, undef;
t { max => 1 }, 1.1, 1.1, { validation => 'max', expected => 1, got => 1.1 };
t { max => 1 }, 'a', 'a', { validation => 'max', error => nerr 'a' };
t { range => [1,2] }, 1, 1, undef;
t { range => [1,2] }, 2, 2, undef;
t { range => [1,2] }, 0.9, 0.9, { validation => 'range', error => { validation => 'min', expected => 1, got => 0.9 } };
t { range => [1,2] }, 2.1, 2.1, { validation => 'range', error => { validation => 'max', expected => 2, got => 2.1 } };
t { num => 1 }, 0, 0;
f { num => 1 }, '-', nerr '-';
f { num => 1 }, '00', nerr '00';
t { num => 1 }, '1', '1';
f { num => 1 }, '1.1.', nerr '1.1.';
f { num => 1 }, '1.-1', nerr '1.-1';
f { num => 1 }, '.1', nerr '.1';
t { num => 1 }, '0.1e5', '0.1e5';
t { num => 1 }, '0.1e+5', '0.1e+5';
f { num => 1 }, '0.1e5.1', nerr '0.1e5.1';
t { int => 1 }, 0, 0;
t { int => 1 }, -123, -123;
f { int => 1 }, -123.1, { validation => 'int', got => -123.1 };
t { uint => 1 }, 0, 0;
t { uint => 1 }, 123, 123;
f { uint => 1 }, -123, { validation => 'uint', got => -123 };
t { min => 1 }, 1, 1;
f { min => 1 }, 0.9, { validation => 'min', expected => 1, got => 0.9 };
f { min => 1 }, 'a', { validation => 'min', error => nerr 'a' };
t { max => 1 }, 1, 1;
f { max => 1 }, 1.1, { validation => 'max', expected => 1, got => 1.1 };
f { max => 1 }, 'a', { validation => 'max', error => nerr 'a' };
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 } };
f { range => [1,2] }, 2.1, { validation => 'range', error => { validation => 'max', expected => 2, got => 2.1 } };
#t { range => [1,2] }, 'a', 'a', { validation => 'range', error => { validation => 'max', error => nerr 'a' } }; # XXX: Error validation type depends on evaluation order
# email template
use utf8;
t { email => 1 }, $_->[1], $_->[1], $_->[0] ? undef : { validation => 'email', got => $_->[1] } for (
[ 0, 'abc.com' ],
[ 0, 'abc@localhost' ],
[ 0, 'abc@10.0.0.' ],
[ 0, 'abc@256.0.0.1' ],
[ 0, '<whoami>@blicky.net' ],
[ 0, 'a @a.com' ],
[ 0, 'a"@a.com' ],
[ 0, 'a@[:]' ],
[ 0, 'a@127.0.0.1' ],
[ 0, 'a@[::1]' ],
[ 1, 'a@a.com' ],
[ 1, 'a@a.com.' ],
[ 1, 'é@yörhel.nl' ],
[ 1, 'a+_0-c@yorhel.nl' ],
[ 1, 'é@x-y_z.example' ],
[ 1, 'abc@x-y_z.example' ],
f { email => 1 }, $_, { validation => 'email', got => $_ } for (
'abc.com',
'abc@localhost',
'abc@10.0.0.',
'abc@256.0.0.1',
'<whoami>@blicky.net',
'a @a.com',
'a"@a.com',
'a@[:]',
'a@127.0.0.1',
'a@[::1]',
);
t { email => 1 }, $_, $_ for (
'a@a.com',
'a@a.com.',
'é@yörhel.nl',
'a+_0-c@yorhel.nl',
'é@x-y_z.example',
'abc@x-y_z.example',
);
my $long = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx.xxxxx';
t { email => 1 }, $long, $long, { validation => 'email', error => { validation => 'maxlength', got => 255, expected => 254 } };
f { email => 1 }, $long, { validation => 'email', error => { validation => 'maxlength', got => 255, expected => 254 } };
# weburl template
t { weburl => 1 }, $_->[1], $_->[1], $_->[0] ? undef : { validation => 'weburl', got => $_->[1] } for (
[ 0, 'http' ],
[ 0, 'http://' ],
[ 0, 'http:///' ],
[ 0, 'http://x/' ],
[ 0, 'http://x/' ],
[ 0, 'http://256.0.0.1/' ],
[ 0, 'http://blicky.net:050/' ],
[ 0, 'ftp//blicky.net/' ],
[ 1, 'http://blicky.net/' ],
[ 1, 'http://blicky.net:50/' ],
[ 1, 'https://blicky.net/' ],
[ 1, 'https://[::1]:80/' ],
[ 1, 'https://l-n.x_.example.com/' ],
[ 1, 'https://blicky.net/?#Who\'d%20ever%22makeaurl_like-this/!idont.know' ],
f { weburl => 1 }, $_, { validation => 'weburl', got => $_ } for (
'http',
'http://',
'http:///',
'http://x/',
'http://x/',
'http://256.0.0.1/',
'http://blicky.net:050/',
'ftp//blicky.net/',
);
t { weburl => 1}, $_, $_ for (
'http://blicky.net/',
'http://blicky.net:50/',
'https://blicky.net/',
'https://[::1]:80/',
'https://l-n.x_.example.com/',
'https://blicky.net/?#Who\'d%20ever%22makeaurl_like-this/!idont.know',
);