File Coverage

blib/lib/FU/Validate.pm
Criterion Covered Total %
statement 233 236 98.7
branch 196 212 92.4
condition 131 159 82.3
subroutine 27 28 96.4
pod 2 4 50.0
total 589 639 92.1


line stmt bran cond sub pod time code
1             package FU::Validate 1.4;
2              
3 2     2   274567 use v5.36;
  2         9  
4 2     2   592 use experimental 'builtin', 'for_list';
  2         4762  
  2         23  
5 2     2   783 use builtin qw/true false blessed trim/;
  2         170  
  2         106  
6 2     2   13 use Carp 'confess';
  2         4  
  2         165  
7 2     2   1111 use FU::Util 'to_bool', 'has_control';
  2         8  
  2         15365  
8              
9              
10             # Unavailable as custom validation names
11             my %builtin = map +($_,1), qw/
12             type
13             default
14             onerror
15             trim allow_control
16             elems sort unique
17             accept_scalar accept_array
18             keys values unknown missing
19             func
20             /;
21              
22             my %type_vals = map +($_,1), qw/scalar hash array any/;
23             my %unknown_vals = map +($_,1), qw/remove reject pass/;
24             my %missing_vals = map +($_,1), qw/create reject ignore/;
25             my %implied_type = qw/
26             accept_array scalar
27             keys hash values hash unknown hash
28             elems array sort array unique array accept_scalar array
29             /;
30             my %sort_vals = (
31             str => sub($x,$y) { $x cmp $y },
32             num => sub($x,$y) { $x <=> $y },
33             );
34              
35 47     47   50 sub _length($exp, $min, $max) {
  47         62  
  47         55  
  47         57  
  47         70  
36 47     47   50 [ func => sub($v) {
  47         62  
  47         38  
37 47 100       107 my $got = ref $v eq 'HASH' ? keys %$v : ref $v eq 'ARRAY' ? @$v : length $v;
    100          
38 47 100 100     216 (!defined $min || $got >= $min) && (!defined $max || $got <= $max) ? 1 : { expected => $exp, got => $got };
39 47         340 }]
40             }
41              
42             # Basically the same as ( regex => $arg ), but hides the regex error
43 22     22   26 sub _reg($reg) {
  22         24  
  22         20  
44 22 100   67   131 ( type => 'scalar', func => sub { $_[0] =~ $reg ? 1 : { got => $_[0] } } );
  67         758  
45             }
46              
47              
48             our $re_num = qr/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
49             our $re_int = qr/^-?(?:0|[1-9][0-9]*)$/;
50             our $re_uint = qr/^(?:0|[1-9][0-9]*)$/;
51             our $re_fqdn = qr/(?:[a-zA-Z0-9][\w-]*\.)+[a-zA-Z][a-zA-Z0-9-]{1,25}\.?/;
52             our $re_ip4_digit = qr/(?:0|[1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/;
53             our $re_ip4 = qr/($re_ip4_digit\.){3}$re_ip4_digit/;
54             # This monstrosity is based on http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses
55             # Doesn't allow IPv4-mapped-IPv6 addresses or other fancy stuff.
56             our $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}|:)/;
57             our $re_ip = qr/(?:$re_ip4|$re_ip6)/;
58             our $re_domain = qr/(?:$re_fqdn|$re_ip4|\[$re_ip6\])/;
59             our $re_email = qr/^[-\+\.#\$=\w]+\@$re_fqdn$/;
60             our $re_weburl = qr/^https?:\/\/$re_domain(?::[1-9][0-9]{0,5})?(?:\/[^\s<>"]*)$/;
61             our $re_date = qr/^(?:19[0-9][0-9]|20[0-9][0-9])-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/;
62              
63              
64             # There's a special '_scalartype' option used for coerce() and empty(), with the following values:
65             # 0/undef/missing: string, 1:num, 2:int, 3:bool
66             # The highest number, i.e. most restrictive type, is chosen when multiple validations exist.
67              
68             our %default_validations = (
69             regex => sub($reg) {
70             # Error objects should be plain data structures so that they can easily
71             # be converted to JSON for debugging. We have to stringify $reg in the
72             # error object to ensure that.
73             +{ type => 'scalar', func => sub { $_[0] =~ $reg ? 1 : { regex => "$reg", got => $_[0] } } }
74             },
75             enum => sub($vals) {
76             my @l = ref $vals eq 'HASH' ? sort keys %$vals : ref $vals eq 'ARRAY' ? @$vals : ($vals);
77             my %opts = map +($_,1), @l;
78             +{ type => 'scalar', func => sub { $opts{ (my $v = $_[0]) } ? 1 : { expected => \@l, got => $_[0] } } }
79             },
80              
81             minlength => sub($v) { _length $v, $v, undef },
82             maxlength => sub($v) { _length $v, undef, $v },
83             length => sub($v) { _length $v, ref $v eq 'ARRAY' ? @$v : ($v, $v) },
84              
85             bool => { _scalartype => 3, type => 'any', func => sub { my $r = to_bool $_[0]; return {} if !defined $r; $_[0] = $r; 1 } },
86             anybool => { _scalartype => 3, type => 'any', default => false, func => sub { $_[0] = $_[0] ? true : false; 1 } },
87              
88             num => [ _scalartype => 1, _reg($re_num), func => sub { $_[0] = $_[0]*1; 1 } ],
89             int => [ _scalartype => 2, _reg($re_int), func => sub { return { message => 'integer out of range' } if $_[0] < -9223372036854775808 || $_[0] > 9223372036854775807; $_[0] = int $_[0]; 1 } ],
90             uint => [ _scalartype => 2, _reg($re_uint), func => sub { return { message => 'integer out of range' } if $_[0] > 18446744073709551615; $_[0] = int $_[0]; 1 } ],
91             min => sub($min) { +{ num => 1, func => sub { $_[0] >= $min ? 1 : { expected => $min, got => $_[0] } } } },
92             max => sub($max) { +{ num => 1, func => sub { $_[0] <= $max ? 1 : { expected => $max, got => $_[0] } } } },
93             range => sub { [ min => $_[0][0], max => $_[0][1] ] },
94              
95             ascii => { _reg qr/^[\x20-\x7E]*$/ },
96             sl => { _reg qr/^[^\t\r\n]+$/ },
97             ipv4 => { _reg $re_ip4 },
98             ipv6 => { _reg $re_ip6 },
99             ip => { _reg $re_ip },
100             email => { _reg($re_email), maxlength => 254 },
101             weburl => { _reg($re_weburl), maxlength => 65536 }, # the maxlength is a bit arbitrary, but better than unlimited
102             date => { _reg $re_date },
103             );
104              
105              
106 592     592   1941 sub _new { bless { validations => [], @_ }, __PACKAGE__ }
107              
108              
109 595     595   734 sub _compile($schema, $custom, $rec, $top, $validations=$top->{validations}) {
  595         801  
  595         753  
  595         771  
  595         698  
  595         887  
  595         744  
110 595         834 my $iscompiled = $schema isa __PACKAGE__;
111              
112             # For hashref schemas, builtins always override other validations
113             $schema = [
114             map +($_, $schema->{$_}),
115             (grep !$builtin{$_}, keys %$schema),
116 595 100       3640 (grep $builtin{$_}, keys %$schema),
117             ] if ref $schema ne 'ARRAY';
118              
119 595         1814 for my($name, $val) (@$schema) {
120 925 100       1971 if ($name eq 'type') {
121 161 100 66     817 confess "Invalid value for 'type': $val" if $name eq 'type' && !$type_vals{$val};
122 160 100 100     742 confess "Incompatible types, the schema specifies '$val', but another validation requires '$top->{type}'" if $top->{type} && $top->{type} ne $val;;
123 158         317 $top->{type} = $val;
124 158         347 next;
125             }
126              
127 764         1230 my $type = $implied_type{$name};
128 764 100       1289 if ($type) {
129 74 50 66     279 confess "Incompatible types, the schema specifies '$top->{type}' but the '$name' validation implies '$type'" if $top->{type} && $top->{type} ne $type;
130 74         215 $top->{type} = $type;
131             }
132              
133 764 100 100     2654 if ($name eq 'elems' || $name eq 'values') {
134 10   66     39 $top->{$name} ||= _new;
135 10         37 _compile($val, $custom, $rec-1, $top->{$name});
136 10         21 next;
137             }
138              
139 754 100       1326 if ($name eq 'keys') {
140 31   100     143 $top->{keys} ||= {};
141 31         127 for my($n,$v) (%$val) {
142 55   66     205 $top->{keys}{$n} ||= _new;
143 55         178 _compile($v, $custom, $rec-1, $top->{keys}{$n});
144             }
145 30         75 next;
146             }
147              
148 723 100       1312 if ($name eq 'func') {
149 226         439 push @$validations, $val;
150 226         531 next;
151             }
152              
153 497 100       926 if ($name eq 'default') {
154 31         67 $top->{default} = $val;
155 31 100 66     105 delete $top->{default} if ref $val eq 'SCALAR' && $$val eq 'required';
156 31         87 next;
157             }
158              
159 466 100       943 if ($name eq '_scalartype') {
160 53 100 100     286 $top->{_scalartype} = $val if ($top->{_scalartype}||0) < $val;
161 53         135 next;
162             }
163              
164 413 100       800 if ($builtin{$name}) {
165 69 50 66     247 confess "Invalid value for 'missing': $val" if $name eq 'missing' && !$missing_vals{$val};
166 69 50 66     213 confess "Invalid value for 'unknown': $val" if $name eq 'unknown' && !$unknown_vals{$val};
167 69 50 66     190 confess "Invalid value for 'accept_array': $val" if $name eq 'accept_array' && $val && $val ne 'first' && $val ne 'last';
      100        
      66        
168 69 100 33     173 $val = $sort_vals{$val} || confess "Unknown value for 'sort': $val" if $name eq 'sort' && ref $val ne 'CODE';
      100        
169 69         136 $top->{$name} = $val;
170 69         223 next;
171             }
172              
173 344 100 66     640 if ($iscompiled && $name eq 'validations') {
174 2         4 push @$validations, @$val;
175 2         3 next;
176             }
177              
178 342   100     952 my $t = $custom->{$name} || $default_validations{$name};
179 342 100       898 confess "Unknown validation: $name" if !$t;
180 340 100       4046 confess "Recursion limit exceeded while resolving validation '$name'" if $rec < 1;
181 338 100       729 $t = ref $t eq 'CODE' ? $t->($val) : $t;
182              
183 338         614 my $v = _new name => $name;
184 338         1134 _compile($t, $custom, $rec-1, $top, $v->{validations});
185 210 100       855 push @$validations, $v if $v->{validations}->@*;
186             }
187             }
188              
189              
190 192     192 0 479713 sub compile($pkg, $schema, $custom={}) {
  192         432  
  192         385  
  192         341  
  192         305  
191 192 50       623 return $schema if $schema isa __PACKAGE__;
192 192         441 my $c = _new;
193 192         634 _compile $schema, $custom, 64, $c;
194 185         683 $c
195             }
196              
197              
198             sub _validate_hash {
199 26     26   44 my $c = $_[0];
200              
201 26 100       65 if ($c->{keys}) {
202 20         31 my @err;
203 20         86 for my ($k, $s) ($c->{keys}->%*) {
204 37 100       85 if (!exists $_[1]{$k}) {
205 15 100 100     88 next if $s->{missing} && $s->{missing} eq 'ignore';
206 7 100 100     42 return { validation => 'missing', key => $k } if $s->{missing} && $s->{missing} eq 'reject';
207 5 50 50     35 $_[1]{$k} = ref $s->{default} eq 'CODE' ? $s->{default}->() : $s->{default} // undef;
208 5 100       12 next if exists $s->{default};
209             }
210              
211 25         66 my $r = _validate($s, $_[1]{$k});
212 25 100       54 if ($r) {
213 5         9 $r->{key} = $k;
214 5         12 push @err, $r;
215             }
216             }
217 18 100       62 return { validation => 'keys', errors => [ sort { $a->{key} cmp $b->{key} } @err ] } if @err;
  0         0  
218             }
219              
220 19 100       99 if ($c->{values}) {
221 2         2 my @err;
222 2         6 for my ($k, $v) ($_[1]->%*) {
223 3         6 my $r = _validate($c->{values}, $v);
224 3 100       5 if ($r) {
225 1         2 $r->{key} = $k;
226 1         2 push @err, $r;
227             }
228             }
229 2 100       12 return { validation => 'values', errors => [ sort { $a->{key} cmp $b->{key} } @err ] } if @err;
  0         0  
230             }
231             }
232              
233             sub _validate_elems {
234 7     7   8 my @err;
235 7         9 for my $i (0..$#{$_[1]}) {
  7         21  
236 13         26 my $r = _validate($_[0]{elems}, $_[1][$i]);
237 13 100       26 if ($r) {
238 3         4 $r->{index} = $i;
239 3         6 push @err, $r;
240             }
241             }
242 7 100       24 return { validation => 'elems', errors => \@err } if @err;
243             }
244              
245              
246             sub _validate_rec {
247 361     361   443 my $c = $_[0];
248 361         878 for my $v ($c->{validations}->@*) {
249 371 100       714 if (ref $v eq 'CODE') {
250 191         444 my $r = $v->($_[1]);
251 191 100       742 return { %$r, validation => 'func' } if ref $r eq 'HASH';
252 135 100       363 return { validation => 'func', result => $r } if !$r;
253             } else {
254 180         312 my $r = _validate_rec($v, $_[1]);
255             return {
256             # If the error was a custom 'func' object, then make that the primary cause.
257             # This makes it possible for validations to provide their own error objects.
258             $r->{validation} eq 'func' && (!exists $r->{result} || keys $r->%* > 2) ? $r->%* : (error => $r),
259             validation => $v->{name},
260 180 100 100     1098 } if $r;
    100          
261             }
262             }
263             }
264              
265              
266             sub _validate_array {
267 23     23   28 my $c = $_[0];
268              
269 23 100       54 $_[1] = [sort { $c->{sort}->($a, $b) } $_[1]->@* ] if $c->{sort};
  13         30  
270              
271             # Key-based uniqueness
272 23 100 66     92 if ($c->{unique} && (!$c->{sort} || ref $c->{unique} eq 'CODE')) {
    100 100        
273 4         6 my %h;
274 4         4 for my $i (0..$#{$_[1]}) {
  4         11  
275 12 100       26 my $k = ref $c->{unique} eq 'CODE' ? $c->{unique}->($_[1][$i]) : $_[1][$i];
276 12 100       49 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};
277 10         23 $h{$k} = $i;
278             }
279              
280             # Comparison-based uniqueness
281             } elsif ($c->{unique}) {
282 2         3 for my $i (0..$#{$_[1]}-1) {
  2         8  
283             return { validation => 'unique', index_a => $i, value_a => $_[1][$i], index_b => $i+1, value_b => $_[1][$i+1] }
284 4 100       8 if $c->{sort}->($_[1][$i], $_[1][$i+1]) == 0
285             }
286             }
287             }
288              
289              
290             sub _validate_input {
291 224     224   314 my $c = $_[0];
292              
293 224   100     599 my $type = $c->{type} // 'scalar';
294              
295             # accept_array (needs to be done before 'trim')
296 1         4 $_[1] = $_[1]->@* == 0 ? undef : $c->{accept_array} eq 'first' ? $_[1][0] : $_[1][ $#{$_[1]} ]
297 224 100 66     582 if $c->{accept_array} && ref $_[1] eq 'ARRAY';
    100          
    100          
298              
299             # early scalar checks
300 224 100 100     999 if (defined $_[1] && !ref $_[1] && $type eq 'scalar') {
      100        
301             # trim needs to be done before the 'default' test
302 137 100 100     853 $_[1] = trim $_[1] =~ s/\r//rg if !exists $c->{trim} || $c->{trim};
303              
304 137 100 100     642 return { validation => 'allow_control' } if !$c->{allow_control} && has_control $_[1];
305             }
306              
307             # default
308 223 100 100     988 if (!defined $_[1] || (!ref $_[1] && $_[1] eq '')) {
      100        
309 26 100       66 if (exists $c->{default}) {
310 12 100       50 $_[1] = ref $c->{default} eq 'CODE' ? $c->{default}->($_[1]) : $c->{default};
311 12         42 return;
312             }
313 14         55 return { validation => 'required' };
314             }
315              
316 197 100       440 if ($type eq 'scalar') {
    100          
    100          
    50          
317 129 100       248 return { validation => 'type', expected => 'scalar', got => lc ref $_[1] } if ref $_[1];
318              
319             } elsif ($type eq 'hash') {
320 30 100 100     96 return { validation => 'type', expected => 'hash', got => lc ref $_[1] || 'scalar' } if ref $_[1] ne 'HASH';
321              
322             # Each branch below makes a shallow copy of the hash, so that further
323             # validations can perform in-place modifications without affecting the
324             # input.
325 27 100 100     196 if (!$c->{keys} || ($c->{unknown} && $c->{unknown} eq 'pass')) {
    100 100        
      100        
326 13         57 $_[1] = { $_[1]->%* };
327             } elsif (!$c->{unknown} || $c->{unknown} eq 'remove') {
328 12         67 $_[1] = { map +($_, $_[1]{$_}), grep $c->{keys}{$_}, keys $_[1]->%* };
329             } else {
330 2         11 my @err = grep !$c->{keys}{$_}, keys $_[1]->%*;
331 2 100       14 return { validation => 'unknown', keys => \@err, expected => [ sort keys $c->{keys}->%* ] } if @err;
332 1         4 $_[1] = { $_[1]->%* };
333             }
334              
335             } elsif ($type eq 'array') {
336 27 100 66     53 $_[1] = [$_[1]] if $c->{accept_scalar} && !ref $_[1];
337 27 50 50     56 return { validation => 'type', expected => $c->{accept_scalar} ? 'array or scalar' : 'array', got => lc ref $_[1] || 'scalar' } if ref $_[1] ne 'ARRAY';
    100          
338 26         48 $_[1] = [$_[1]->@*]; # Create a shallow copy to prevent in-place modification.
339              
340             } elsif ($type eq 'any') {
341             # No need to do anything here.
342             }
343              
344             ($type eq 'hash' && &_validate_hash) ||
345 191 100 100     873 ($c->{elems} && &_validate_elems) ||
      100        
      100        
      100        
      100        
346             &_validate_rec ||
347             ($type eq 'array' && &_validate_array)
348             }
349              
350              
351             sub _validate {
352 224     224   368 my $c = $_[0];
353 224         360 my $r = &_validate_input;
354             ($r, $_[1]) = (undef, ref $c->{onerror} eq 'CODE' ? $c->{onerror}->($_[0], bless $r, 'FU::Validate::err') : $c->{onerror})
355 224 100 100     589 if $r && exists $c->{onerror};
    100          
356 224         448 $r
357             }
358              
359              
360 183     183 0 257 sub validate($c, $input) {
  183         255  
  183         272  
  183         238  
361 183         487 my $r = _validate($c, $input);
362 183 100       505 return $input if !$r;
363 79         225 $r = bless $r, 'FU::Validate::err';;
364 79         231 my @e = $r->errors;
365 79 100       14387 $r->{longmess} = Carp::longmess(@e > 1 ? join("\n",@e)."\n" : $e[0]);
366 79         952 die $r;
367             }
368              
369              
370             sub coerce {
371 14     14 1 43 my $c = $_[0];
372 14         23 my %opt = @_[2..$#_];
373 14 100 66     66 if (!defined $_[1]) {
    100 66        
    100 33        
    100 66        
    100 66        
374 2         3 $_[1] = undef;
375             } elsif ($c->{_scalartype}) {
376 6 100       21 $_[1] = $c->{_scalartype} == 3 ? !!$_[1] : $c->{_scalartype} == 2 ? int $_[1] : $_[1]+0;
    100          
377             } elsif (!$c->{type} || $c->{type} eq 'scalar') {
378 1         3 $_[1] = "$_[1]";
379             } elsif ($c->{type} eq 'array' && $c->{elems} && ref $_[1] eq 'ARRAY') {
380 1         6 coerce($c->{elems}, $_, %opt) for $_[1]->@*;
381             } elsif ($c->{type} eq 'hash' && $c->{keys} && ref $_[1] eq 'HASH') {
382 2   66     7 $opt{unknown} ||= $c->{unknown};
383 1         3 delete @{$_[1]}{ grep !$c->{keys}{$_}, keys $_[1]->%* }
384 2 100 66     13 if $opt{unknown} && $opt{unknown} ne 'pass';
385             $_[1]{$_} = exists $_[1]{$_} ? coerce($c->{keys}{$_}, $_[1]{$_}, %opt) : empty($c->{keys}{$_})
386 2 100       15 for keys $c->{keys}->%*;
387             }
388 14         123 return $_[1];
389             }
390              
391              
392 13     13 1 20 sub empty($c) {
  13         13  
  13         16  
393 13 50       33 return ref $c->{default} eq 'CODE' ? $c->{default}->(undef) : $c->{default} if exists $c->{default};
    100          
394 9 100 66     32 return [] if $c->{type} && $c->{type} eq 'array';
395 7 50 66     35 return $c->{keys} ? +{ map +($_, empty($c->{keys}{$_})), keys $c->{keys}->%* } : {} if $c->{type} && $c->{type} eq 'hash';
    100          
396 6 100 66     36 return undef if $c->{type} && $c->{type} eq 'any';
397             # Only scalar types remain
398 4 50       18 return !$c->{_scalartype} ? '' : $c->{_scalartype} == 3 ? !1 : 0;
    50          
399             }
400              
401              
402              
403 18 50   18   25 sub _fmtkey($k) { $k =~ /^[a-zA-Z0-9_-]+$/ ? $k : FU::Util::json_format($k); }
  18         25  
  18         22  
  18         158  
404 24     24   63 sub _fmtval($v) { eval { $v = FU::Util::json_format($v) }; "$v" }
  24         39  
  24         27  
  24         74  
  24         222  
  24         262  
405 18     18   26 sub _inval($t,$v) { sprintf 'invalid %s: %s', $t, _fmtval $v }
  18         35  
  18         31  
  18         71  
  18         54  
406              
407             # validation name => formatting sub
408             # TODO: document.
409             our %error_format = (
410             required => sub { 'required value missing' },
411             allow_control => sub { 'invalid control character' },
412             type => sub($e) { "invalid type, expected '$e->{expected}' but got '$e->{got}'" },
413             unknown => sub($e) { sprintf 'unknown key%s: %s', $e->{keys}->@* == 1 ? '' : 's', join ', ', map _fmtkey($_), $e->{keys}->@* },
414             minlength => sub($e) { sprintf "input too short, expected minimum of %d but got %d", $e->{expected}, $e->{got} },
415             maxlength => sub($e) { sprintf "input too long, expected maximum of %d but got %d", $e->{expected}, $e->{got} },
416             length => sub($e) {
417             !ref $e->{expected}
418             ? sprintf 'invalid input length, expected %d but got %d', $e->{expected}, $e->{got}
419             : sprintf 'invalid input length, expected between %d and %d but got %d', $e->{expected}->@*, $e->{got}
420             },
421             num => sub($e) { _inval 'number', $e->{got} },
422             min => sub($e) { $e->{error} ? _inval 'number', $e->{error}{got} : sprintf 'expected minimum %s but got %s', $e->{expected}, $e->{got} },
423             max => sub($e) { $e->{error} ? _inval 'number', $e->{error}{got} : sprintf 'expected maximum %s but got %s', $e->{expected}, $e->{got} },
424             range => sub($e) { FU::Validate::err::errors($e->{error}) },
425             );
426              
427              
428             package FU::Validate::err;
429 2     2   50 use v5.36;
  2         9  
430              
431 2 0   2   1730 use overload '""' => sub { $_[0]{longmess} || join "\n", $_[0]->errors };
  2     0   4420  
  2         21  
  0         0  
432              
433             # TODO: document.
434 193     193   323 sub errors($e, $prefix='') {
  193         271  
  193         302  
  193         251  
435 193         412 my $val = $e->{validation};
436 193 100       447 my $p = $prefix ? "$prefix: " : '';
437             $FU::Validate::error_format{$val} ? map "$p$_", $FU::Validate::error_format{$val}->($e) :
438             $val eq 'keys' ? map errors($_, $prefix.'.'.FU::Validate::_fmtkey($_->{key})), $e->{errors}->@* :
439             $val eq 'values' ? map errors($_, $prefix.'.'.FU::Validate::_fmtkey($_->{key})), $e->{errors}->@* :
440             $val eq 'missing' ? $prefix.'.'.FU::Validate::_fmtkey($e->{key}).': required key missing' :
441             $val eq 'elems' ? map errors($_, $prefix."[$_->{index}]"), $e->{errors}->@* :
442             $val eq 'unique' ? $prefix."[$e->{index_b}] value '".FU::Validate::_fmtval($e->{value_a})."' duplicated" :
443             $e->{error} ? errors($e->{error}, "${p}validation '$val'") :
444 193 50       1229 $e->{message} ? "${p}validation '$val': $e->{message}" :
    100          
    100          
    100          
    100          
    100          
    100          
    100          
445             "${p}failed validation '$val'";
446             }
447              
448              
449             1;
450             __END__