summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2015-09-19 14:27:21 +0200
committerYorhel <git@yorhel.nl>2015-09-19 14:29:06 +0200
commit4223f4a6f345de8df0dd311ee624baee69f8202d (patch)
treecb98546f126d879a81c0c70a99353cea8ff24c70
parent81933d9acb53e368d2dd0f735b0e4d3f8971007f (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.pm40
-rw-r--r--lib/TUWF/Misc.pod102
-rw-r--r--t/kv_validate.t167
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)
+ );
}