File Coverage

blib/lib/TUWF/Validate.pm
Criterion Covered Total %
statement 165 166 99.4
branch 116 120 96.6
condition 69 72 95.8
subroutine 28 28 100.0
pod 0 3 0.0
total 378 389 97.1


line stmt bran cond sub pod time code
1             package TUWF::Validate;
2              
3 3     3   146052 use strict;
  3         14  
  3         98  
4 3     3   24 use warnings;
  3         8  
  3         90  
5 3     3   25 use Carp 'croak';
  3         4  
  3         155  
6 3     3   18 use Exporter 'import';
  3         4  
  3         119  
7 3     3   22 use Scalar::Util 'blessed';
  3         6  
  3         10933  
8              
9             our @EXPORT_OK = qw/compile validate/;
10             our $VERSION = '1.5';
11              
12              
13             # Unavailable as custom validation names
14             my %builtin = map +($_,1), qw/
15             type
16             required default
17             onerror
18             rmwhitespace
19             values scalar sort unique
20             keys unknown
21             func
22             /;
23              
24              
25             sub _length {
26 99     99   173 my($exp, $min, $max) = @_;
27             +{ _analyze_minlength => $min, _analyze_maxlength => $max, func => sub {
28 94 100   94   247 my $got = ref $_[0] eq 'HASH' ? keys %{$_[0]} : ref $_[0] eq 'ARRAY' ? @{$_[0]} : length $_[0];
  6 100       18  
  4         9  
29 94 100 100     481 (!defined $min || $got >= $min) && (!defined $max || $got <= $max) ? 1 : { expected => $exp, got => $got };
30             }}
31 99         657 }
32              
33             # Basically the same as ( regex => $arg ), but hides the regex error
34             sub _reg {
35 27     27   41 my $reg = $_[0];
36 27 100   128   177 ( type => 'scalar', _analyze_regex => "$reg", func => sub { $_[0] =~ $reg ? 1 : { got => $_[0] } } );
  128         1008  
37             }
38              
39              
40             our $re_num = qr/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
41             my $re_int = qr/^-?(?:0|[1-9]\d*)$/;
42             our $re_uint = qr/^(?:0|[1-9]\d*)$/;
43             my $re_fqdn = qr/(?:[a-zA-Z0-9][\w-]*\.)+[a-zA-Z][a-zA-Z0-9-]{1,25}\.?/;
44             my $re_ip4_digit = qr/(?:0|[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/;
45             my $re_ip4 = qr/($re_ip4_digit\.){3}$re_ip4_digit/;
46             # This monstrosity is based on http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
47             # Doesn't allow IPv4-mapped-IPv6 addresses or other fancy stuff.
48             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}|:)/;
49             my $re_ip = qr/(?:$re_ip4|$re_ip6)/;
50             my $re_domain = qr/(?:$re_fqdn|$re_ip4|\[$re_ip6\])/;
51             # Also used by the TUWF::Misc::kv_validate()
52             our $re_email = qr/^[-\+\.#\$=\w]+\@$re_fqdn$/;
53             our $re_weburl = qr/^https?:\/\/$re_domain(?::[1-9][0-9]{0,5})?(?:\/[^\s<>"]*)$/;
54              
55              
56             our %default_validations = (
57             regex => sub {
58             my $reg = shift;
59             # Error objects should be plain data structures so that they can easily
60             # be converted to JSON for debugging. We have to stringify $reg in the
61             # error object to ensure that.
62             +{ type => 'scalar', _analyze_regex => "$reg", func => sub { $_[0] =~ $reg ? 1 : { regex => "$reg", got => $_[0] } } }
63             },
64             enum => sub {
65             my @l = ref $_[0] eq 'HASH' ? sort keys %{$_[0]} : ref $_[0] eq 'ARRAY' ? @{$_[0]} : ($_[0]);
66             my %opts = map +($_,1), @l;
67             +{ type => 'scalar', func => sub { $opts{ (my $v = $_[0]) } ? 1 : { expected => \@l, got => $_[0] } } }
68             },
69              
70             minlength => sub { _length $_[0], $_[0] },
71             maxlength => sub { _length $_[0], undef, $_[0] },
72             length => sub { _length($_[0], ref $_[0] eq 'ARRAY' ? @{$_[0]} : ($_[0], $_[0])) },
73              
74             anybool => { type => 'any', required => 0, default => 0, func => sub { $_[0] = $_[0] ? 1 : 0; 1 } },
75             undefbool => { type => 'any', required => 0, default => undef, func => sub { $_[0] = $_[0] ? 1 : 0; 1 } },
76             jsonbool => { type => 'any', func => sub {
77             my $r = $_[0];
78             blessed $r && (
79             $r->isa('JSON::PP::Boolean')
80             || $r->isa('JSON::XS::Boolean')
81             || $r->isa('Types::Serialiser::Boolean')
82             || $r->isa('Cpanel::JSON::XS::Boolean')
83             || $r->isa('boolean')
84             ) ? 1 : {};
85             } },
86              
87             # JSON number format, regex from http://stackoverflow.com/questions/13340717/json-numbers-regular-expression
88             num => { _reg $re_num },
89             int => { _reg $re_int }, # implies num
90             uint => { _reg $re_uint }, # implies num
91             min => sub {
92             my $min = shift;
93             +{ num => 1, _analyze_min => $min, func => sub { $_[0] >= $min ? 1 : { expected => $min, got => $_[0] } } }
94             },
95             max => sub {
96             my $max = shift;
97             +{ num => 1, _analyze_max => $max, func => sub { $_[0] <= $max ? 1 : { expected => $max, got => $_[0] } } }
98             },
99             range => sub { +{ min => $_[0][0], max => $_[0][1] } },
100              
101             ascii => { _reg qr/^[\x20-\x7E]*$/ },
102             ipv4 => { _reg $re_ip4 },
103             ipv6 => { _reg $re_ip6 },
104             ip => { _reg $re_ip },
105             email => { _reg($re_email), maxlength => 254 },
106             weburl => { _reg($re_weburl), maxlength => 65536 }, # the maxlength is a bit arbitrary, but better than unlimited
107             );
108              
109              
110             # Loads a hashref of validations and a schema definition, and converts it into
111             # an object with:
112             # {
113             # name => $name_or_undef,
114             # validations => [ $recursive_compiled_object, .. ],
115             # schema => $modified_schema_without_validations,
116             # known_keys => { $key => 1, .. } # Extracted from 'keys', Used for the 'unknown' validation
117             # }
118             sub _compile {
119 1017     1017   1930 my($validations, $schema, $rec) = @_;
120              
121 1017         1319 my(%top, @val);
122 1017 100       2088 my @keys = keys %{$schema->{keys}} if $schema->{keys};
  47         161  
123              
124 1017         3048 for(sort keys %$schema) {
125 1944 100 100     6143 if($builtin{$_} || /^_analyze_/) {
126 1372         2378 $top{$_} = $schema->{$_};
127 1372         2113 next;
128             }
129              
130 572   100     1562 my $t = $validations->{$_} || $default_validations{$_};
131 572 100       1245 croak "Unknown validation: $_" if !$t;
132 570 100       1725 croak "Recursion limit exceeded while resolving validation '$_'" if $rec < 1;
133 568 100       1308 $t = ref $t eq 'HASH' ? $t : $t->($schema->{$_});
134              
135 568         1480 my $v = _compile($validations, $t, $rec-1);
136 440         806 $v->{name} = $_;
137 440         1044 push @val, $v;
138             }
139              
140             # Inherit some builtin options from validations
141 885         1621 for my $t (@val) {
142 440 100 100     1100 if($top{type} && $t->{schema}{type} && $top{type} ne $t->{schema}{type}) {
      100        
143 2 50       169 croak "Incompatible types, the schema specifies '$top{type}' but validation '$t->{name}' requires '$t->{schema}{type}'" if $schema->{type};
144 0         0 croak "Incompatible types, '$t->[0]' requires '$t->{schema}{type}', but another validation requires '$top{type}'";
145             }
146             exists $t->{schema}{$_} and !exists $top{$_} and $top{$_} = delete $t->{schema}{$_}
147 438   100     4793 for qw/required default onerror rmwhitespace type scalar unknown sort unique/;
      66        
148              
149 438         650 push @keys, keys %{ delete $t->{known_keys} };
  438         889  
150 438 100       1162 push @keys, keys %{ $t->{schema}{keys} } if $t->{schema}{keys};
  16         50  
151             }
152              
153             # Compile sub-schemas
154 883 100       1566 $top{keys} = { map +($_, compile($validations, $top{keys}{$_})), keys %{$top{keys}} } if $top{keys};
  47         169  
155 882 100       1557 $top{values} = compile($validations, $top{values}) if $top{values};
156              
157             # XXX: Flattening recursive validations would be faster and may simplify
158             # the code a bit, but makes error objects harder to interpret.
159              
160             # XXX: As an optimization, it's possible to remove double validations (e.g.
161             # multiple invocations of the same validation with the same options due to
162             # validations calling each other). Care must be taken that this won't
163             # affect error objects (i.e. only subsequent invocations should be
164             # removed).
165              
166             return {
167 882         3977 validations => \@val,
168             schema => \%top,
169             known_keys => { map +($_,1), @keys },
170             };
171             }
172              
173              
174             sub compile {
175 449     449 0 25494 my($validations, $schema) = @_;
176              
177 449 50       1230 return $schema if ref $schema eq __PACKAGE__;
178              
179 449         904 my $c = _compile $validations, $schema, 64;
180              
181 442   100     1323 $c->{schema}{type} //= 'scalar';
182 442   100     1531 $c->{schema}{required} //= 1;
183 442   100     1589 $c->{schema}{rmwhitespace} //= 1;
184 442   100     1419 $c->{schema}{unknown} //= 'remove';
185              
186 442 100       835 if(exists $c->{schema}{sort}) {
187 10         17 my $s = $c->{schema}{sort};
188             $c->{schema}{sort} =
189             ref $s eq 'CODE' ? $s
190 6     6   14 : $s eq 'str' ? sub { $_[0] cmp $_[1] }
191 22     22   70 : $s eq 'num' ? sub { $_[0] <=> $_[1] }
192 10 50       58 : croak "Unknown value for 'sort': $c->{schema}{sort}";
    100          
    100          
193             }
194 442 100 100 12   938 $c->{schema}{unique} = sub { $_[0] } if $c->{schema}{unique} && !ref $c->{schema}{unique} && !$c->{schema}{sort};
  12   100     18  
195              
196 442         1280 bless $c, __PACKAGE__;
197             }
198              
199              
200             sub _validate_rec {
201 660     660   1034 my($c, $input) = @_;
202              
203             # hash keys
204 660 100       1145 if($c->{schema}{keys}) {
205 32         41 my @err;
206 32         41 for my $k (keys %{$c->{schema}{keys}}) {
  32         86  
207             # We need to overload the '!exists && !required && !default'
208             # scenario a bit, because in that case we should not create the key
209             # in the output. All other cases will be handled just fine by
210             # passing an implicit 'undef'.
211 44         84 my $s = $c->{schema}{keys}{$k};
212 44 100 100     162 next if !exists $input->{$k} && !$s->{schema}{required} && !exists $s->{schema}{default};
      100        
213              
214 30         62 my $r = _validate($s, $input->{$k});
215 30         60 $input->{$k} = $r->[0];
216 30 100       71 if($r->[1]) {
217 8         15 $r->[1]{key} = $k;
218 8         22 push @err, $r->[1];
219             }
220             }
221 32 100       93 return [$input, { validation => 'keys', errors => \@err }] if @err;
222             }
223              
224             # array values
225 652 100       1111 if($c->{schema}{values}) {
226 10         17 my @err;
227 10         26 for my $i (0..$#$input) {
228 18         36 my $r = _validate($c->{schema}{values}, $input->[$i]);
229 18         26 $input->[$i] = $r->[0];
230 18 100       42 if($r->[1]) {
231 2         4 $r->[1]{index} = $i;
232 2         8 push @err, $r->[1];
233             }
234             }
235 10 100       26 return [$input, { validation => 'values', errors => \@err }] if @err;
236             }
237              
238             # validations
239 650         744 for (@{$c->{validations}}) {
  650         1200  
240 344         576 my $r = _validate_rec($_, $input);
241 344         618 $input = $r->[0];
242              
243             return [$input, {
244             # If the error was a custom 'func' object, then make that the primary cause.
245             # This makes it possible for validations to provide their own error objects.
246 104         584 $r->[1]{validation} eq 'func' && (!exists $r->[1]{result} || keys %{$r->[1]} > 2) ? %{$r->[1]} : (error => $r->[1]),
247             validation => $_->{name},
248 344 100 100     1108 }] if $r->[1];
    100          
249             }
250              
251             # func
252 526 100       1007 if($c->{schema}{func}) {
253 306         658 my $r = $c->{schema}{func}->($input);
254 306 100       1215 return [$input, { %$r, validation => 'func' }] if ref $r eq 'HASH';
255 202 100       376 return [$input, { validation => 'func', result => $r }] if !$r;
256             }
257              
258 420         841 return [$input]
259             }
260              
261              
262             sub _validate_array {
263 200     200   320 my($c, $input) = @_;
264              
265 200 100       623 return [$input] if $c->{schema}{type} ne 'array';
266              
267 44 100       107 $input = [sort { $c->{schema}{sort}->($a,$b) } @$input ] if $c->{schema}{sort};
  26         61  
268              
269             # Key-based uniqueness
270 44 100 100     147 if($c->{schema}{unique} && ref $c->{schema}{unique} eq 'CODE') {
    100          
271 8         13 my %h;
272 8         19 for my $i (0..$#$input) {
273 24         49 my $k = $c->{schema}{unique}->($input->[$i]);
274 24 100       102 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};
275 20         38 $h{$k} = $i;
276             }
277              
278             # Comparison-based uniqueness
279             } elsif($c->{schema}{unique}) {
280 4         25 for my $i (0..$#$input-1) {
281             return [$input, { validation => 'unique', index_a => $i, value_a => $input->[$i], index_b => $i+1, value_b => $input->[$i+1] }]
282 8 100       19 if $c->{schema}{sort}->($input->[$i], $input->[$i+1]) == 0
283             }
284             }
285              
286 38         93 return [$input]
287             }
288              
289              
290             sub _validate_input {
291 373     373   560 my($c, $input) = @_;
292              
293             # rmwhitespace (needs to be done before the 'required' test)
294 373 100 100     1773 if(defined $input && !ref $input && $c->{schema}{type} eq 'scalar' && $c->{schema}{rmwhitespace}) {
      100        
      100        
295 212         513 $input =~ s/\r//g;
296 212         913 $input =~ s/^\s*//;
297 212         903 $input =~ s/\s*$//;
298             }
299              
300             # required & default
301 373 100 100     1504 if(!defined $input || (!ref $input && $input eq '')) {
      100        
302             # XXX: This will return undef if !required and no default is set, even for hash and array types. Should those get an empty hash or array?
303 44 100       172 return [ref $c->{schema}{default} eq 'CODE' ? $c->{schema}{default}->($input) : exists $c->{schema}{default} ? $c->{schema}{default} : $input] if !$c->{schema}{required};
    100          
    100          
304 22         106 return [$input, { validation => 'required' }];
305             }
306              
307 329 100       824 if($c->{schema}{type} eq 'scalar') {
    100          
    100          
    100          
308 210 100       373 return [$input, { validation => 'type', expected => 'scalar', got => lc ref $input }] if ref $input;
309              
310             } elsif($c->{schema}{type} eq 'hash') {
311 46 100 100     132 return [$input, { validation => 'type', expected => 'hash', got => lc ref $input || 'scalar' }] if ref $input ne 'HASH';
312              
313             # unknown
314             # Each branch below makes a shallow copy of the hash, so that further
315             # validations can perform in-place modifications without affecting the
316             # input.
317 40 100       106 if($c->{schema}{unknown} eq 'remove') {
    100          
318 20         72 $input = { map +($_, $input->{$_}), grep $c->{known_keys}{$_}, keys %$input };
319             } elsif($c->{schema}{unknown} eq 'reject') {
320 4         17 my @err = grep !$c->{known_keys}{$_}, keys %$input;
321 4 100       14 return [$input, { validation => 'unknown', keys => \@err, expected => [ sort keys %{$c->{known_keys}} ] }] if @err;
  2         13  
322 2         8 $input = { %$input };
323             } else {
324 16         51 $input = { %$input };
325             }
326              
327             } elsif($c->{schema}{type} eq 'array') {
328 50 100 66     117 $input = [$input] if $c->{schema}{scalar} && !ref $input;
329 50 50 50     115 return [$input, { validation => 'type', expected => $c->{schema}{scalar} ? 'array or scalar' : 'array', got => lc ref $input || 'scalar' }] if ref $input ne 'ARRAY';
    100          
330 48         94 $input = [@$input]; # Create a shallow copy to prevent in-place modification.
331              
332             } elsif($c->{schema}{type} eq 'any') {
333             # No need to do anything here.
334              
335             } else {
336 1         119 croak "Unknown type '$c->{schema}{type}'"; # Should be checked in _compile(), preferably.
337             }
338              
339 316         594 my $r = _validate_rec($c, $input);
340 316 100       718 return $r if $r->[1];
341 200         289 $input = $r->[0];
342              
343 200         357 _validate_array($c, $input);
344             }
345              
346              
347             sub _validate {
348 373     373   587 my($c, $input) = @_;
349 373         636 my $r = _validate_input($c, $input);
350 372 100 100     2261 $r->[1] && exists $c->{schema}{onerror} ? [ref $c->{schema}{onerror} eq 'CODE' ? $c->{schema}{onerror}->(bless $r, 'TUWF::Validate::Result') : $c->{schema}{onerror}] : $r
    100          
351             }
352              
353              
354             sub validate {
355 325 100   325 0 85669 my($c, $input) = ref $_[0] eq __PACKAGE__ ? @_ : (compile($_[0], $_[1]), $_[2]);
356 325         633 bless _validate($c, $input), 'TUWF::Validate::Result';
357             }
358              
359              
360             sub analyze {
361 41     41 0 236 require TUWF::Validate::Interop;
362 41         122 TUWF::Validate::Interop::analyze($_[0]);
363             }
364              
365              
366              
367             package TUWF::Validate::Result;
368              
369 3     3   35 use strict;
  3         6  
  3         102  
370 3     3   18 use warnings;
  3         5  
  3         118  
371 3     3   20 use Carp 'croak';
  3         6  
  3         262  
372              
373             # A result object contains: [$data, $error]
374              
375             # In boolean context, returns whether the validation succeeded.
376 3     3   3840 use overload bool => sub { !$_[0][1] };
  3     162   3142  
  3         27  
  162         1365  
377              
378             # Returns the validation errors, or undef if validation succeeded
379 324     324   22668 sub err { $_[0][1] }
380              
381             # Returns the validated and normalized input, dies if validation didn't succeed.
382             sub data {
383 162 100   162   391 if($_[0][1]) {
384 69         1014 require Data::Dumper;
385 69         7278 my $s = Data::Dumper->new([$_[0][1]])->Terse(1)->Pair(':')->Indent(0)->Sortkeys(1)->Dump;
386 69         10861 croak "Validation failed: $s";
387             }
388 93         341 $_[0][0]
389             }
390              
391             # Same as 'data', but returns partially validated and normalized data if validation failed.
392 324     324   176204 sub unsafe_data { $_[0][0] }
393              
394             # TODO: Human-readable error message formatting
395              
396             1;