From cbebc3a21eb5cc832109671466676aff275a26b9 Mon Sep 17 00:00:00 2001 From: Yorhel Date: Wed, 5 Mar 2025 15:32:01 +0100 Subject: [PATCH] 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. --- FU.pm | 5 +- FU/Validate.pm | 284 ++++++++++++++++----------------------- t/validate.t | 353 +++++++++++++++++++++++++------------------------ 3 files changed, 295 insertions(+), 347 deletions(-) diff --git a/FU.pm b/FU.pm index a0cd38e..4d04562 100644 --- a/FU.pm +++ b/FU.pm @@ -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}, @_; } diff --git a/FU/Validate.pm b/FU/Validate.pm index 1538c5a..7f1cc1c 100644 --- a/FU/Validate.pm +++ b/FU/Validate.pm @@ -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 below) and C<$validations> is an optional hashref containing -L that C<$schema> can refer to. +L 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. +C 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 and C 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 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 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 if validation succeeded, an error object otherwise. - -An error object is a hashref containing at least one key: I, 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 below. - -=back +An error is thrown if the input does not validate. The error object is a +C-blessed hashref containing at least one key: +I, 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 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, which accepts either the string I or I. 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 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 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, I and C built-in options are validated separately diff --git a/t/validate.t b/t/validate.t index c7eb459..fdcd416 100644 --- a/t/validate.t +++ b/t/validate.t @@ -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, '@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', + '@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', );