diff options
author | Yorhel <git@yorhel.nl> | 2015-09-19 14:27:21 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2015-09-19 14:29:06 +0200 |
commit | 4223f4a6f345de8df0dd311ee624baee69f8202d (patch) | |
tree | cb98546f126d879a81c0c70a99353cea8ff24c70 | |
parent | 81933d9acb53e368d2dd0f735b0e4d3f8971007f (diff) |
kv_validate(): Pass fields to 'func's and add some default templates
!!!BACKWARDS INCOMPATIBLE CHANGE!!!
This removes the min and max fields, and instead replaces them with a
'num' template. They're mostly equivalent, but means that a template
field needs to be added when min or max are used. The returned error is
also different, as it now returns [field, 'template', 'int'] instead of
[field, 'min', 10] or something.
The documentation of kv_validate() is pretty awful.
-rw-r--r-- | lib/TUWF/Misc.pm | 40 | ||||
-rw-r--r-- | lib/TUWF/Misc.pod | 102 | ||||
-rw-r--r-- | t/kv_validate.t | 167 |
3 files changed, 249 insertions, 60 deletions
diff --git a/lib/TUWF/Misc.pm b/lib/TUWF/Misc.pm index 75f2839..3230d3c 100644 --- a/lib/TUWF/Misc.pm +++ b/lib/TUWF/Misc.pm @@ -23,8 +23,37 @@ sub uri_escape { } + + +sub _template_validate_num { + $_[0] *= 1; # Normalize to perl number + return 0 if defined($_[1]{min}) && $_[0] < $_[1]{min}; + return 0 if defined($_[1]{max}) && $_[0] > $_[1]{max}; + return 1; +} + +my $re_fqdn = qr/(?:[a-zA-Z0-9][\w-]*\.)+[a-zA-Z][a-zA-Z0-9-]{1,25}\.?/; +my $re_ip4_digit = qr/(?:0|[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/; +my $re_ip4 = qr/($re_ip4_digit\.){3}$re_ip4_digit/; +# This monstrosity is based on http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses +# Doesn't allow IPv4-mapped-IPv6 addresses or other fancy stuff. +my $re_ip6 = qr/(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)/; +my $re_domain = qr/(?:$re_fqdn|$re_ip4|\[$re_ip6\])/; + +my %default_templates = ( + # JSON number format, regex from http://stackoverflow.com/questions/13340717/json-numbers-regular-expression + num => { func => \&_template_validate_num, regex => qr/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/, inherit => ['min','max'] }, + int => { func => \&_template_validate_num, regex => qr/^-?(?:0|[1-9]\d*)$/, inherit => ['min','max'] }, + uint => { func => \&_template_validate_num, regex => qr/^(?:0|[1-9]\d*)$/, inherit => ['min','max'] }, + ascii => { regex => qr/^[\x20-\x7E]*$/ }, + email => { regex => qr/^[-\+\.!#\$=\w]+\@$re_domain$/, maxlength => 254 }, + weburl => { regex => qr/^https?:\/\/$re_domain(?::[1-9][0-9]{0,5})?\/[^\s<>"]*$/, maxlength => 65536 }, # the maxlength is a bit arbitrary, but better than unlimited +); + + sub kv_validate { my($sources, $templates, $params) = @_; + $templates = { %default_templates, %$templates }; my @err; my %ret; @@ -80,17 +109,18 @@ sub _validate { # value, \%templates, \%rules # length return 'minlength' if $r->{minlength} && length $v < $r->{minlength}; return 'maxlength' if $r->{maxlength} && length $v > $r->{maxlength}; - # min/max - return 'min' if defined($r->{min}) && (!looks_like_number($v) || $v < $r->{min}); - return 'max' if defined($r->{max}) && (!looks_like_number($v) || $v > $r->{max}); # enum return 'enum' if $r->{enum} && !grep $_ eq $v, @{$r->{enum}}; # regex return 'regex' if $r->{regex} && (ref($r->{regex}) eq 'ARRAY' ? ($v !~ m/$r->{regex}[0]/) : ($v !~ m/$r->{regex}/)); # template - return 'template' if $r->{template} && _validate($_[0], $t, $t->{$r->{template}}); + if($r->{template}) { + my $in = $t->{$r->{template}}{inherit}; + my %r = (($in ? (map exists($r->{$_}) ? ($_,$r->{$_}) : (), @$in) : ()), %{$t->{$r->{template}}}); + return 'template' if _validate($_[0], $t, \%r); + } # function - return 'func' if $r->{func} && (ref($r->{func}) eq 'ARRAY' ? !$r->{func}[0]->($_[0]) : !$r->{func}->($_[0])); + return 'func' if $r->{func} && (ref($r->{func}) eq 'ARRAY' ? !$r->{func}[0]->($_[0], $r) : !$r->{func}->($_[0], $r)); # passed validation return undef; } diff --git a/lib/TUWF/Misc.pod b/lib/TUWF/Misc.pod index 9517adb..0348778 100644 --- a/lib/TUWF/Misc.pod +++ b/lib/TUWF/Misc.pod @@ -60,7 +60,7 @@ represent multiple values. This function is rarely used directly, C<formValidate()> does everything you need when validating common input data. I<sources> is a hashref explaining where the values should be fetched from. -Each key in the hash represents the I<name> of the source, and it's value is a +Each key in the hash represents the I<name> of the source, and its value is a subroutine reference. This subroutine should accept one argument: the name of the field, and is expected return a list of values, or an empty list if there are no values with that key. The following example defines a source by the name @@ -120,15 +120,6 @@ Number. Minimum length of the value. Arrayref. The value must be equal to any of the strings in the array. Note that even though a string comparison is used, this works fine numbers as well. -=item max - -Number. The value must be lower than the indicated number (using numeric -comparison). This option implies that the value must be a number. - -=item min - -Number. See I<max>. - =item regex Validate the value against a regular expression. For identification, this @@ -139,12 +130,24 @@ therefore be used in your code. =item func -Subroutine reference. Validate the value against a function. The subroutine -should return false if the value is invalid. Since the actual value variable is -passed to the subroutine, it is allowed to modify it in-place. This can be -useful for normalizing or de-serializing values. This constraint is always -executed last. This option can also be set to an arrayref, which works the same -as with the I<regex> option. +Subroutine reference. Validate the value against a function. The subroutine is +passed two arguments: The value, and a hash reference with the I<fields> in the +scope that the I<func> is defined. The subroutine should return false if the +value is invalid, true otherwise. The value argument is passed as a reference, +and may be modified in-place to perform normalization. This option can also be +set to an arrayref, which works the same as with the I<regex> option. This +constraint is only executed after the other constraints have been validated. So +the subroutine is not called if, for example, the value is empty and the +I<required> flag is set. + +The extra I<fields> argument can be used to read additional information for +validation. For example, + + { func => sub { $_[0] =~ /^$_[1]{prefix}/ }, + prefix => 'hello' } + +The subroutine verifies that the value is prefixed by the value of the +I<prefix> field, which is passed to the subroutine as second argument. =item template @@ -152,6 +155,29 @@ String, refers to a key in the I<templates> hash. Validates the value against the options in I<%{$templates{$string}}>, which may contain any of the above mentioned options (except <source name>). +When a template contains a I<func> field, the fields that are passed to the +subroutine only include the fields that are specified in the template options. +Using the example above, the following will not work as expected: + + $templates{prefix} = { func => sub { $_[0] =~ /^$_[1]{prefix} } }; + # In the field definitions + { template => 'prefix', prefix => 'hello' } + +In this example, the only field that is passed to the subroutine is the 'func' +field that is in the template definition itself. The subroutine has no access +to 'prefix' field that is defined outside of the template. To work around this +issue, a special I<inherit> option exists to allow the template to inherit +certain fields from the parent scope. The template definition can be changed as +follows: + + $templates{prefix} = { + func => sub { $_[0] =~ /^$_[1]{prefix} }, + inherit => ['prefix'], + }; + +This causes the 'prefix' field from the user of the template to be passed into +the context of the template itself, so the subroutine can access it. + =item multi 0/1, indicates whether there should be only one value or multiple values. If @@ -179,7 +205,47 @@ an arrayref where each item represents an invalid field. Each invalid field is represented again by an arrayref containing three items: the name of the field, the option that caused it to fail and the value of that option. -Example: +The following templates are provided by default. These templates can be safely +overridden with the I<templates> argument to C<kv_validate> or removed +completely by setting the template to C<undef>. + +=over + +=item num + +The value must be a (JSON-like) number. Two extra fields are available when +this template is used: I<min> to specify the lower bound on the number, and +I<max> the upper bound. This template automatically coorces the value into a +Perl number. + +=item int + +Similar to the I<num> template, except the value must be a whole number, no +fractions or exponents are allowed. + +=item uint + +Similar to the I<int> template, but the number must be positive. + +=item ascii + +The value must consist entirely of printable ASCII characters. + +=item email + +The value must be a valid e-mail address. Note that this is just simple +validation using a regular expression. A valid e-mail address in this context +does not imply that the email exists, or that all mail clients or servers will +accept it. For more precise validation, there's always L<Data::Validate::Email> +and L<Email::Address>. + +=item weburl + +The value must be a valid HTTP or HTTPS URL. + +=back + +Usage example: my $r = kv_validate( # sources @@ -198,7 +264,7 @@ Example: ], ); - # $r will look something like: + # Depending on the input, $r may look something like: { name => 'John Doe', age => 28, diff --git a/t/kv_validate.t b/t/kv_validate.t index 1e97839..612f423 100644 --- a/t/kv_validate.t +++ b/t/kv_validate.t @@ -2,6 +2,7 @@ use strict; use warnings; +use utf8; my @tests; my %templates; @@ -48,35 +49,6 @@ BEGIN{@tests=( [ name => ' ' ], { name => '', _err => [[ 'name', 'required', 1 ]] }, - # min / max - { param => 'age', min => 0, max => 0 }, - [ age => 0 ], - { age => 0 }, - - { param => 'age', min => 0, max => 0 }, - [ age => 1 ], - { age => 1, _err => [[ 'age', 'max', 0 ]] }, - - { param => 'age', min => 0, max => 0 }, - [ age => 0.5 ], - { age => 0.5, _err => [[ 'age', 'max', 0 ]] }, - - { param => 'age', min => 0, max => 0 }, - [ age => -1 ], - { age => -1, _err => [[ 'age', 'min', 0 ]] }, - - { param => 'age', min => 0, max => 1000 }, - [ age => '1e3' ], - { age => '1e3' }, - - { param => 'age', min => 0, max => 1000 }, - [ age => '1e4' ], - { age => '1e4', _err => [[ 'age', 'max', 1000 ]] }, - - { param => 'age', min => 0, max => 0 }, - [ age => 'x' ], - { age => 'x', _err => [[ 'age', 'min', 0 ]] }, - # minlength / maxlength { param => 'msg', minlength => 2 }, [ msg => 'ab' ], @@ -132,9 +104,9 @@ BEGIN{@tests=( [ board => '' ], { board => [''], _err => [[ 'board', 'required', 1 ]] }, - { param => 'board', multi => 1, min => 1 }, + { param => 'board', multi => 1, template => 'int', min => 1 }, [ board => 0 ], - { board => [0], _err => [[ 'board', 'min', 1 ]] }, + { board => [0], _err => [[ 'board', 'template', 'int' ]] }, { param => 'board', multi => 1, maxcount => 1 }, [ board => 1 ], @@ -172,16 +144,16 @@ BEGIN{@tests=( )}, # func - do { my $f = sub { $_[0] =~ y/a-z/A-Z/; $_[0] =~ /^X/ }; ( - { param => 't', func => $f }, + do { my $f = sub { $_[0] =~ y/a-z/A-Z/; $_[0] =~ /^$_[1]{start}/ }; ( + { param => 't', func => $f, start => 'X' }, [ t => 'xyz' ], { t => 'XYZ' }, - { param => 't', func => $f }, + { param => 't', func => $f, start => 'X' }, [ t => 'zyx' ], { t => 'ZYX', _err => [[ 't', 'func', $f ]] }, - { param => 't', func => [$f,1,2,3] }, + { param => 't', func => [$f,1,2,3], start => 'X' }, [ t => 'zyx' ], { t => 'ZYX', _err => [[ 't', 'func', [$f,1,2,3] ]] }, )}, @@ -190,11 +162,12 @@ BEGIN{@tests=( do { $templates{hex} = { regex => qr/^[0-9a-f]+$/i }; $templates{crc32} = { template => 'hex', minlength => 8, maxlength => 8 }; + $templates{prefix} = { func => sub { $_[0] =~ /^$_[1]{prefix}/ }, inherit => ['prefix'] }; ()}, { param => 'crc', template => 'hex' }, [ crc => '12345678' ], { crc => '12345678' }, - + { param => 'crc', template => 'crc32' }, [ crc => '12345678' ], { crc => '12345678' }, @@ -206,6 +179,122 @@ BEGIN{@tests=( { param => 'crc', template => 'crc32' }, [ crc => '123456789' ], { crc => '123456789', _err => [[ 'crc', 'template', 'crc32' ]] }, + + { param => 'x', template => 'prefix', prefix => 'he' }, + [ x => 'hello world' ], + { x => 'hello world' }, + + { param => 'x', template => 'prefix', prefix => 'he' }, + [ x => 'hullo' ], + { x => 'hullo', _err => [[ 'x', 'template', 'prefix' ]] }, + + # num / int / uint templates + { param => 'age', template => 'num' }, + [ age => 0 ], + { age => 0 }, + + { param => 'age', template => 'num' }, + [ age => '0.5' ], + { age => 0.5 }, + + { param => 'age', template => 'num' }, + [ age => '0.5e3' ], + { age => 500 }, + + { param => 'age', template => 'num' }, + [ age => '-0.5E-3' ], + { age => -0.0005 }, + + { param => 'age', template => 'int' }, + [ age => '0.5e10' ], + { age => '0.5e10', _err => [[ 'age', 'template', 'int' ]] }, + + { param => 'age', template => 'num' }, + [ age => '0600' ], + { age => '0600', _err => [[ 'age', 'template', 'num' ]] }, + + { param => 'age', template => 'uint' }, + [ age => '50' ], + { age => 50 }, + + { param => 'age', template => 'uint' }, + [ age => '-1' ], + { age => -1, _err => [[ 'age', 'template', 'uint' ]] }, + + { param => 'age', template => 'num', min => 0, max => 0 }, + [ age => '0' ], + { age => 0 }, + + { param => 'age', template => 'num', max => 0 }, + [ age => '0.5' ], + { age => 0.5, _err => [[ 'age', 'template', 'num' ]] }, + + { param => 'age', template => 'int', max => 0 }, + [ age => 1 ], + { age => 1, _err => [[ 'age', 'template', 'int' ]] }, + + { param => 'age', template => 'uint', max => 0 }, + [ age => 1 ], + { age => 1, _err => [[ 'age', 'template', 'uint' ]] }, + + { param => 'age', template => 'num', min => 1 }, + [ age => 0 ], + { age => 0, _err => [[ 'age', 'template', 'num' ]] }, + + { param => 'age', template => 'int', min => 1 }, + [ age => 0 ], + { age => 0, _err => [[ 'age', 'template', 'int' ]] }, + + { param => 'age', template => 'uint', min => 1 }, + [ age => 0 ], + { age => 0, _err => [[ 'age', 'template', 'uint' ]] }, + + # email template + (map +( + { param => 'mail', template => 'email' }, + [ mail => $_->[1] ], + { mail => $_->[1], $_->[0] ? () : (_err => [[ 'mail', 'template', 'email' ]]) }, + ), + [ 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, 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@xxxxxxxxxxxxxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx.xxxxx' ], + [ 1, 'a@a.com' ], + [ 1, 'a@a.com.' ], + [ 1, 'a@127.0.0.1' ], + [ 1, 'a@[::1]' ], + [ 1, 'é@yörhel.nl' ], + [ 1, 'a+_0-c@yorhel.nl' ], + [ 1, 'é@x-y_z.example' ], + [ 1, 'abc@x-y_z.example' ], + ), + + # weburl template + (map +( + { param => 'url', template => 'weburl' }, + [ url => $_->[1] ], + { url => $_->[1], $_->[0] ? () : (_err => [[ 'url', 'template', 'weburl' ]]) }, + ), + [ 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' ], + ), )} use Test::More tests => 1+@tests/3; @@ -219,5 +308,9 @@ sub getfield { for my $i (0..$#tests/3) { my($fields, $params, $exp) = ($tests[$i*3], $tests[$i*3+1], $tests[$i*3+2]); - is_deeply(kv_validate({ param => sub { getfield($_[0], $params) } }, \%templates, [$fields]), $exp, 'Test '.($i+1)); + is_deeply( + kv_validate({ param => sub { getfield($_[0], $params) } }, \%templates, [$fields]), + $exp, + sprintf '%s = %s', $fields->{param}, join ',', getfield($fields->{param}, $params) + ); } |