File Coverage

blib/lib/Data/Domain.pm
Criterion Covered Total %
statement 660 703 93.8
branch 279 342 81.5
condition 108 155 69.6
subroutine 138 146 94.5
pod 5 5 100.0
total 1190 1351 88.0


line stmt bran cond sub pod time code
1             #======================================================================
2             package Data::Domain; # documentation at end of file
3             #======================================================================
4 4     4   419974 use 5.010;
  4         41  
5 4     4   23 use strict;
  4         9  
  4         90  
6 4     4   18 use warnings;
  4         9  
  4         102  
7 4     4   30 use Carp;
  4         8  
  4         212  
8 4     4   1811 use Data::Dumper;
  4         20133  
  4         212  
9 4     4   1940 use Scalar::Does 0.007;
  4         473156  
  4         39  
10 4     4   2302 use Scalar::Util ();
  4         8  
  4         79  
11 4     4   18 use Try::Tiny;
  4         10  
  4         236  
12 4     4   2216 use List::MoreUtils qw/part natatime any/;
  4         39315  
  4         27  
13 4     4   4942 use if $] < 5.037, experimental => 'smartmatch'; # smartmatch no longer experimental after 5.037
  4         11  
  4         31  
14 4 50       47 use overload '""' => \&_stringify,
15 4     4   16045 $] < 5.037 ? ('~~' => \&_matches) : (); # fully deprecated, so cannot be overloaded
  4         10  
16 4     4   2154 use match::simple ();
  4         7300  
  4         493  
17              
18             our $VERSION = "1.11";
19              
20             our $MESSAGE; # global var for last message from _matches()
21             our $MAX_DEEP = 100; # limit for recursive calls to inspect()
22              
23             #----------------------------------------------------------------------
24             # exports
25             #----------------------------------------------------------------------
26              
27             # lists of symbols to export
28             my @CONSTRUCTORS;
29             my %SHORTCUTS;
30              
31             BEGIN {
32 4     4   22 @CONSTRUCTORS = qw/Whatever Empty
33             Num Int Nat Date Time String Handle
34             Enum List Struct One_of All_of/;
35 4         464 %SHORTCUTS = (
36             True => [ -true => 1 ],
37             False => [ -true => 0 ],
38             Defined => [ -defined => 1 ],
39             Undef => [ -defined => 0 ],
40             Blessed => [ -blessed => 1 ],
41             Unblessed => [ -blessed => 0 ],
42             Ref => [ -ref => 1 ],
43             Unref => [ -ref => 0 ],
44             Regexp => [ -does => 'Regexp' ],
45             Obj => [ -blessed => 1 ],
46             Class => [ -package => 1 ],
47             );
48             }
49              
50             # setup exports through Sub::Exporter API
51             use Sub::Exporter -setup => {
52             exports => [ 'node_from_path', # no longer documented, but still present for backwards compat
53 56         101 (map {$_ => \&_wrap_domain } @CONSTRUCTORS ),
54 4         15 (map {$_ => \&_wrap_shortcut_options} keys %SHORTCUTS) ],
  44         127  
55             groups => { constructors => \@CONSTRUCTORS,
56             shortcuts => [keys %SHORTCUTS] },
57             collectors => { INIT => \&_sub_exporter_init },
58             installer => \&_sub_exporter_installer,
59 4     4   2537 };
  4         44050  
60              
61             # customize Sub::Exporter to support "bang-syntax" for excluding symbols
62             # see https://rt.cpan.org/Public/Bug/Display.html?id=80234
63             { my @dont_export;
64              
65             # detect symbols prefixed by '!' and remember them in @dont_export
66             sub _sub_exporter_init {
67 4     4   427 my ($collection, $context) = @_;
68 4         8 my $args = $context->{import_args};
69             my ($exclude, $regular_args)
70 4 100 66 5   76 = part {!ref $_->[0] && $_->[0] =~ /^!/ ? 0 : 1} @$args;
  5         63  
71 4         20 @$args = @$regular_args;
72 4         11 @dont_export = map {substr($_->[0], 1)} @$exclude;
  1         3  
73 4         15 1;
74             }
75              
76             # install symbols, except those that belong to @dont_export
77             sub _sub_exporter_installer {
78 4     4   32 my ($arg, $to_export) = @_;
79 4         57 my %export_hash = @$to_export;
80 4         17 delete @export_hash{@dont_export};
81 4         88 Sub::Exporter::default_installer($arg, [%export_hash]);
82             }
83             }
84              
85             # constructors group : for each domain constructor, we export a closure
86             # that just calls new() on the corresponding subclass. For example,
87             # Num(@args) is just equivalent to Data::Domain::Num->new(@args).
88             sub _wrap_domain {
89 56     56   3063 my ($class, $name, $args, $coll) = @_;
90 56     131   256 return sub {return "Data::Domain::$name"->new(@_)};
  131         63227  
91             }
92              
93              
94             # # shortcuts group : calling 'Whatever' with various pre-built options
95             sub _wrap_shortcut_options {
96 44     44   1471 my ($class, $name, $args, $coll) = @_;
97 44     14   167 return sub {return Data::Domain::Whatever->new(@{$SHORTCUTS{$name}}, @_)};
  14         6370  
  14         72  
98             }
99              
100              
101              
102             #----------------------------------------------------------------------
103             # messages
104             #----------------------------------------------------------------------
105              
106             my $builtin_msgs = {
107             english => {
108             Generic => {
109             UNDEFINED => "undefined data",
110             INVALID => "invalid",
111             TOO_SMALL => "smaller than minimum '%s'",
112             TOO_BIG => "bigger than maximum '%s'",
113             EXCLUSION_SET => "belongs to exclusion set",
114             MATCH_TRUE => "data true/false",
115             MATCH_ISA => "is not a '%s'",
116             MATCH_CAN => "does not have method '%s'",
117             MATCH_DOES => "does not do '%s'",
118             MATCH_BLESSED => "data blessed/unblessed",
119             MATCH_PACKAGE => "data is/is not a package",
120             MATCH_REF => "is/is not a reference",
121             MATCH_SMART => "does not smart-match '%s'",
122             MATCH_ISWEAK => "weak/strong reference",
123             MATCH_READONLY=> "readonly data",
124             MATCH_TAINTED => "tainted/untainted",
125             },
126             Whatever => {
127             MATCH_DEFINED => "data defined/undefined",
128             },
129             Num => {INVALID => "invalid number",},
130             Date => {INVALID => "invalid date",},
131             String => {
132             TOO_SHORT => "less than %d characters",
133             TOO_LONG => "more than %d characters",
134             SHOULD_MATCH => "should match '%s'",
135             SHOULD_NOT_MATCH => "should not match '%s'",
136             },
137             Handle => {INVALID => "is not an open filehandle"},
138             Enum => {NOT_IN_LIST => "not in enumeration list",},
139             List => {
140             NOT_A_LIST => "is not an arrayref",
141             TOO_SHORT => "less than %d items",
142             TOO_LONG => "more than %d items",
143             ANY => "should have at least one '%s'",
144             },
145             Struct => {
146             NOT_A_HASH => "is not a hashref",
147             FORBIDDEN_FIELD => "contains forbidden field: '%s'"
148             },
149             },
150              
151             "français" => {
152             Generic => {
153             UNDEFINED => "donnée non définie",
154             INVALID => "incorrect",
155             TOO_SMALL => "plus petit que le minimum '%s'",
156             TOO_BIG => "plus grand que le maximum '%s'",
157             EXCLUSION_SET => "fait partie des valeurs interdites",
158             MATCH_TRUE => "donnée vraie/fausse",
159             MATCH_ISA => "n'est pas un '%s'",
160             MATCH_CAN => "n'a pas la méthode '%s'",
161             MATCH_DOES => "ne se comporte pas comme un '%s'",
162             MATCH_BLESSED => "donnée blessed/unblessed",
163             MATCH_PACKAGE => "est/n'est pas un package",
164             MATCH_REF => "est/n'est pas une référence",
165             MATCH_SMART => "n'obéit pas au smart-match '%s'",
166             MATCH_ISWEAK => "référence weak/strong",
167             MATCH_READONLY=> "donnée readonly",
168             MATCH_TAINTED => "tainted/untainted",
169             },
170             Whatever => {
171             MATCH_DEFINED => "donnée définie/non définie",
172             },
173             Num => {INVALID => "nombre incorrect",},
174             Date => {INVALID => "date incorrecte",},
175             String => {
176             TOO_SHORT => "moins de %d caractères",
177             TOO_LONG => "plus de %d caractères",
178             SHOULD_MATCH => "devrait être reconnu par la regex '%s'",
179             SHOULD_NOT_MATCH => "ne devrait pas être reconnu par la regex '%s'",
180             },
181             Handle => {INVALID => "n'est pas une filehandle ouverte"},
182             Enum => {NOT_IN_LIST => "n'appartient pas à la liste énumérée",},
183             List => {
184             NOT_A_LIST => "n'est pas une arrayref",
185             TOO_SHORT => "moins de %d éléments",
186             TOO_LONG => "plus de %d éléments",
187             ANY => "doit avoir au moins un '%s'",
188             },
189             Struct => {
190             NOT_A_HASH => "n'est pas une hashref",
191             FORBIDDEN_FIELD => "contient le champ interdit: '%s'",
192             },
193             },
194             };
195              
196             # inherit Int and Nat messages from Num messages
197             foreach my $language (keys %$builtin_msgs) {
198             $builtin_msgs->{$language}{$_} = $builtin_msgs->{$language}{Num}
199             for qw/Int Nat/;
200             }
201              
202             # default messages : english
203             my $global_msgs = $builtin_msgs->{english};
204              
205             #----------------------------------------------------------------------
206             # PUBLIC METHODS
207             #----------------------------------------------------------------------
208              
209             sub messages { # private class method
210 3     3 1 7024 my ($class, $new_messages) = @_;
211 3 50 33     20 croak "messages() is a class method in Data::Domain"
212             if ref $class or $class ne 'Data::Domain';
213              
214             $global_msgs = (ref $new_messages) ? $new_messages
215 3 100       17 : $builtin_msgs->{$new_messages}
    50          
216             or croak "no such builtin messages ($new_messages)";
217             }
218              
219              
220             sub inspect {
221 982     982 1 3450 my ($self, $data, $context) = @_;
222 4     4   7613 no warnings 'recursion';
  4         10  
  4         5564  
223              
224 982 100       1788 if (!defined $data) {
225             # success if data was optional;
226 33 100       80 return if $self->{-optional};
227              
228             # only the 'Whatever' domain can accept undef; other domains will fail
229 26 100       151 return $self->msg(UNDEFINED => '')
230             unless $self->isa("Data::Domain::Whatever");
231             }
232             else { # if $data is defined
233             # check some general properties
234 949 100       2455 if (my $isa = $self->{-isa}) {
235 2     2   69 try {$data->isa($isa)}
236 2 100       13 or return $self->msg(MATCH_ISA => $isa);
237             }
238 948 100       1784 if (my $role = $self->{-does}) {
239 4 100       14 does($data, $role)
240             or return $self->msg(MATCH_DOES => $role);
241             }
242 946 100       2521 if (my $can = $self->{-can}) {
243 3 100       11 $can = [$can] unless does($can, 'ARRAY');
244 3         737 foreach my $method (@$can) {
245 5     5   128 try {$data->can($method)}
246 5 100       39 or return $self->msg(MATCH_CAN => $method);
247             }
248             }
249 945 100       1697 if (my $match_target = $self->{-matches}) {
250 2 100       12 match::simple::match($data, $match_target)
251             or return $self->msg(MATCH_SMART => $match_target);
252             }
253 944 100       1528 if ($self->{-has}) {
254             # EXPERIMENTAL: check methods results
255 1         7 my @msgs = $self->_check_has($data, $context);
256 1 50       8 return {HAS => \@msgs} if @msgs;
257             }
258 943 100       1632 if (defined $self->{-blessed}) {
259             return $self->msg(MATCH_BLESSED => $self->{-blessed})
260 8 100 100     53 if Scalar::Util::blessed($data) xor $self->{-blessed};
261             }
262 939 100       1593 if (defined $self->{-package}) {
263             return $self->msg(MATCH_PACKAGE => $self->{-package})
264 3 100 100     35 if (!ref($data) && $data->isa($data)) xor $self->{-package};
      50        
265             }
266 937 50       1517 if (defined $self->{-isweak}) {
267             return $self->msg(MATCH_ISWEAK => $self->{-isweak})
268 0 0 0     0 if Scalar::Util::isweak($data) xor $self->{-isweak};
269             }
270 937 50       1531 if (defined $self->{-readonly}) {
271             return $self->msg(MATCH_READONLY => $self->{-readonly})
272 0 0 0     0 if Scalar::Util::readonly($data) xor $self->{-readonly};
273             }
274 937 50       1673 if (defined $self->{-tainted}) {
275             return $self->msg(MATCH_TAINTED => $self->{-tainted})
276 0 0 0     0 if Scalar::Util::readonly($data) xor $self->{-tainted};
277             }
278             }
279              
280             # properties that must be checked against both defined and undef data
281 946 100       1614 if (defined $self->{-true}) {
282             return $self->msg(MATCH_TRUE => $self->{-true})
283 11 100 100     56 if $data xor $self->{-true};
284             }
285 941 100       1604 if (defined $self->{-ref}) {
286             return $self->msg(MATCH_REF => $self->{-ref})
287 6 100 100     42 if ref $data xor $self->{-ref};
288             }
289              
290             # now call domain-specific _inspect()
291 938         2448 return $self->_inspect($data, $context)
292             }
293              
294              
295             sub _check_has {
296 1     1   3 my ($self, $data, $context) = @_;
297              
298 1         2 my @msgs;
299 1         1 my $iterator = natatime 2, @{$self->{-has}};
  1         13  
300 1         9 while (my ($meth_to_call, $expectation) = $iterator->()) {
301 3 100       23 my ($meth, @args) = does($meth_to_call, 'ARRAY') ? @$meth_to_call
302             : ($meth_to_call);
303 3         824 my $msg;
304 3 50       9 if (does($expectation, 'ARRAY')) {
305 0     0   0 $msg = try {my @result = $data->$meth(@args);
306 0         0 my $domain = List(@$expectation);
307 0         0 $domain->inspect(\@result)}
308 0     0   0 catch {(my $error_msg = $_) =~ s/\bat\b.*//s; $error_msg};
  0         0  
  0         0  
309             }
310             else {
311 3     3   158 $msg = try {my $result = $data->$meth(@args);
312 2         47 $expectation->inspect($result)}
313 3     1   285 catch {(my $error_msg = $_) =~ s/\bat\b.*//s; $error_msg};
  1         23  
  1         5  
314             }
315 3 100       58 push @msgs, $meth_to_call => $msg if $msg;
316             }
317 1         8 return @msgs;
318             }
319              
320              
321              
322             sub _check_returns {
323 0     0   0 my ($self, $data, $context) = @_;
324              
325 0         0 my @msgs;
326 0         0 my $iterator = natatime 2, @{$self->{-returns}};
  0         0  
327 0         0 while (my ($args, $expectation) = $iterator->()) {
328 0         0 my $msg;
329 0 0       0 if (does($expectation, 'ARRAY')) {
330 0     0   0 $msg = try {my @result = $data->(@$args);
331 0         0 my $domain = List(@$expectation);
332 0         0 $domain->inspect(\@result)}
333 0     0   0 catch {(my $error_msg = $_) =~ s/\bat\b.*//s; $error_msg};
  0         0  
  0         0  
334             }
335             else {
336 0     0   0 $msg = try {my $result = $data->(@$args);
337 0         0 $expectation->inspect($result)}
338 0     0   0 catch {(my $error_msg = $_) =~ s/\bat\b.*//s; $error_msg};
  0         0  
  0         0  
339             }
340 0 0       0 push @msgs, $args => $msg if $msg;
341             }
342 0         0 return @msgs;
343             }
344              
345              
346              
347              
348             #----------------------------------------------------------------------
349             # METHODS FOR INTERNAL USE
350             #----------------------------------------------------------------------
351              
352              
353             sub msg {
354 240     240 1 1868 my ($self, $msg_id, @args) = @_;
355 240         452 my $msgs = $self->{-messages};
356 240         473 my $subclass = $self->subclass;
357 240   66     809 my $name = $self->{-name} || $subclass;
358 240         296 my $msg;
359              
360             # perl v5.22 and above warns if there are too many @args for sprintf.
361             # The line below prevents that warning
362 4     4   31 no if $] ge '5.022000', warnings => 'redundant';
  4         9  
  4         32  
363              
364             # if there is a user_defined message, return it
365 240 100       476 if (defined $msgs) {
366 11         26 for (ref $msgs) {
367 11 100       61 /^CODE/ and return $msgs->($msg_id, @args); # user function
368 10 100       69 /^$/ and return "$name: $msgs"; # user constant string
369 2 50       8 /^HASH/ and do { $msg = $msgs->{$msg_id} # user hash of msgs
  2 50       22  
370             and return sprintf "$name: $msg", @args;
371 0         0 last; # not found in this hash - revert to $global_msgs
372             };
373 0         0 croak "invalid -messages option"; # otherwise
374             }
375             }
376              
377             # otherwise, try global messages
378 229 100       471 return $global_msgs->($msg_id, @args) if ref $global_msgs eq 'CODE';
379             $msg = $global_msgs->{$subclass}{$msg_id} # otherwise
380 228 50 66     764 || $global_msgs->{Generic}{$msg_id}
381             or croak "no error string for message $msg_id";
382              
383 228         1302 return sprintf "$name: $msg", @args;
384             }
385              
386              
387             sub subclass { # returns the class name without initial 'Data::Domain::'
388 367     367 1 546 my ($self) = @_;
389 367   33     769 my $class = ref($self) || $self;
390 367         1468 (my $subclass = $class) =~ s/^Data::Domain:://;
391 367         1823 return $subclass;
392             }
393              
394              
395             sub _expand_range {
396 127     127   237 my ($self, $range_field, $min_field, $max_field) = @_;
397 127   66     756 my $name = $self->{-name} || $self->subclass;
398              
399             # the range field will be replaced by min and max fields
400 127 100       370 if (my $range = delete $self->{$range_field}) {
401 13         25 for ($min_field, $max_field) {
402 26 50       57 not defined $self->{$_}
403             or croak "$name: incompatible options: $range_field / $_";
404             }
405 13 50 33     38 does($range, 'ARRAY') and @$range == 2
406             or croak "$name: invalid argument for $range";
407 13         314 @{$self}{$min_field, $max_field} = @$range;
  13         43  
408             }
409             }
410              
411              
412             sub _check_min_max {
413 126     126   258 my ($self, $min_field, $max_field, $cmp_func) = @_;
414              
415             # choose the appropriate comparison function
416 126 100   13   303 if ($cmp_func eq '<=') {$cmp_func = sub {$_[0] <= $_[1]}}
  88 100       285  
  13 50       53  
417 25     4   86 elsif ($cmp_func eq 'le') {$cmp_func = sub {$_[0] le $_[1]}}
  4         16  
418             elsif (does($cmp_func, 'CODE')) {} # already a coderef, do nothing
419 0         0 else {croak "inappropriate cmp_func for _check_min_max"}
420              
421             # check that min is smaller than max
422 126         1858 my ($min, $max) = @{$self}{$min_field, $max_field};
  126         277  
423 126 100 100     459 if (defined $min && defined $max) {
424 21 100       42 $cmp_func->($min, $max)
425             or croak $self->subclass . ": incompatible min/max values ($min/$max)";
426             }
427             }
428              
429              
430             sub _build_subdomain {
431 469     469   827 my ($self, $domain, $context) = @_;
432 4     4   3065 no warnings 'recursion';
  4         9  
  4         3240  
433              
434             # avoid infinite loop
435 469 100       630 @{$context->{path}} < $MAX_DEEP
  469         3930  
436             or croak "inspect() deepness exceeded $MAX_DEEP; "
437             . "modify \$Data::Domain::MAX_DEEP if you need more";
438              
439 468 100       1147 if (does($domain, 'Data::Domain')) {
    100          
    50          
440             # already a domain, nothing to do
441             }
442             elsif (does($domain, 'CODE')) {
443             # this is a lazy domain, need to call the coderef to get a real domain
444 230     230   9105 $domain = try {$domain->($context)}
445             catch { # remove "at source_file, line ..." from error message
446 1     1   251 (my $error_msg = $_) =~ s/\bat\b.*//s;
447             # return an empty domain that reports the error message
448 1         8 Data::Domain::Empty->new(-name => "domain parameters",
449             -messages => $error_msg);
450 230         8035 };
451             # did we really get a domain ?
452 230 50       3214 does($domain, "Data::Domain")
453             or croak "lazy domain coderef returned an invalid domain";
454             }
455             elsif (!ref $domain) {
456             # this is a scalar, build a constant domain with that single value
457 6 100       271 my $subclass = Scalar::Util::looks_like_number($domain) ? 'Num' : 'String';
458 6         28 $domain = "Data::Domain::$subclass"->new(-min => $domain,
459             -max => $domain,
460             -name => "constant $subclass");
461             }
462             else {
463 0         0 croak "unknown subdomain : $domain";
464             }
465              
466 468         8073 return $domain;
467             }
468              
469              
470             #----------------------------------------------------------------------
471             # UTILITY FUNCTIONS (NOT METHODS)
472             #----------------------------------------------------------------------
473              
474             # valid options for all subclasses
475             my @common_options = qw/-optional -name -messages
476             -true -isa -can -does -matches -ref
477             -has -returns
478             -blessed -package -isweak -readonly -tainted/;
479              
480             sub _parse_args {
481 153     153   308 my ($args_ref, $options_ref, $default_option, $arg_type) = @_;
482              
483 153         208 my %parsed;
484              
485             # parse named arguments
486 153   100     780 while (@$args_ref and $args_ref->[0] =~ /^-/) {
487 123 50   518   574 any {$args_ref->[0] eq $_} (@$options_ref, @common_options)
  518         822  
488             or croak "invalid argument: $args_ref->[0]";
489 123         411 my ($key, $val) = (shift @$args_ref, shift @$args_ref);
490 123         464 $parsed{$key} = $val;
491             }
492              
493             # remaining arguments are mapped to the default option
494 153 100       492 if (@$args_ref) {
495 24 50       49 $default_option or croak "too many args to new()";
496 24 50       52 not exists $parsed{$default_option}
497             or croak "can't have default args if $default_option is set";
498 24 50       82 $parsed{$default_option}
    100          
499             = $arg_type eq 'scalar' ? $args_ref->[0]
500             : $arg_type eq 'arrayref' ? $args_ref
501             : croak "unknown type for default option: $arg_type";
502             }
503              
504 153         296 return \%parsed;
505             }
506              
507              
508             sub node_from_path { # no longer documented, but still present for backwards compat
509 0     0 1 0 my ($root, $path0, @path) = @_;
510 0 0       0 return $root if not defined $path0;
511 0 0       0 return undef if not defined $root;
512 0 0       0 return node_from_path($root->{$path0}, @path)
513             if does($root, 'HASH');
514 0 0       0 return node_from_path($root->[$path0], @path)
515             if does($root, 'ARRAY');
516              
517             # otherwise
518 0         0 croak "node_from_path: incorrect root/path";
519             }
520              
521             #----------------------------------------------------------------------
522             # implementation for overloaded operators
523             #----------------------------------------------------------------------
524             sub _matches {
525 2     2   1183 my ($self, $data, $call_order) = @_;
526 2         6 $Data::Domain::MESSAGE = $self->inspect($data);
527 2         10 return !$Data::Domain::MESSAGE; # smart match successful if no error message
528             }
529              
530             sub _stringify {
531 217     217   2588 my ($self) = @_;
532 217         567 my $dumper = Data::Dumper->new([$self])->Indent(0)->Terse(1);
533 217         7180 return $dumper->Dump;
534             }
535              
536              
537              
538             #======================================================================
539             package Data::Domain::Whatever;
540             #======================================================================
541 4     4   33 use strict;
  4         7  
  4         136  
542 4     4   25 use warnings;
  4         9  
  4         107  
543 4     4   22 use Carp;
  4         7  
  4         250  
544 4     4   78 use Scalar::Does qw/does/;
  4         8  
  4         31  
545             our @ISA = 'Data::Domain';
546              
547             sub new {
548 26     26   52 my $class = shift;
549 26         50 my @options = qw/-defined/;
550 26         62 my $self = Data::Domain::_parse_args( \@_, \@options );
551 26         51 bless $self, $class;
552              
553             not ($self->{-defined } && $self->{-optional})
554 26 50 66     136 or croak "both -defined and -optional: meaningless!";
555              
556 26         96 return $self;
557             }
558              
559             sub _inspect {
560 32     32   63 my ($self, $data) = @_;
561              
562 32 100       69 if (defined $self->{-defined}) {
563             return $self->msg(MATCH_DEFINED => $self->{-defined})
564 9 100 100     59 if defined($data) xor $self->{-defined};
565             }
566              
567             # otherwise, success
568 27         119 return;
569             }
570              
571              
572             #======================================================================
573             package Data::Domain::Empty;
574             #======================================================================
575 4     4   3055 use strict;
  4         30  
  4         82  
576 4     4   20 use warnings;
  4         9  
  4         102  
577 4     4   19 use Carp;
  4         8  
  4         550  
578             our @ISA = 'Data::Domain';
579              
580             sub new {
581 2     2   5 my $class = shift;
582 2         7 my @options = ();
583 2         8 my $self = Data::Domain::_parse_args( \@_, \@options );
584 2         12 bless $self, $class;
585             }
586              
587             sub _inspect {
588 5     5   11 my ($self, $data) = @_;
589              
590 5         16 return $self->msg(INVALID => ''); # always fails
591             }
592              
593              
594             #======================================================================
595             package Data::Domain::Num;
596             #======================================================================
597 4     4   26 use strict;
  4         57  
  4         82  
598 4     4   17 use warnings;
  4         9  
  4         82  
599 4     4   19 use Carp;
  4         8  
  4         183  
600 4     4   22 use Scalar::Util qw/looks_like_number/;
  4         8  
  4         141  
601 4     4   40 use Try::Tiny;
  4         8  
  4         1516  
602              
603             our @ISA = 'Data::Domain';
604              
605             sub new {
606 50     50   81 my $class = shift;
607 50         118 my @options = qw/-range -min -max -not_in/;
608 50         116 my $self = Data::Domain::_parse_args(\@_, \@options);
609 50         93 bless $self, $class;
610              
611 50         142 $self->_expand_range(qw/-range -min -max/);
612 50         133 $self->_check_min_max(qw/-min -max <=/);
613              
614 49 100       112 if ($self->{-not_in}) {
615 1     1   28 try {my $vals = $self->{-not_in};
616 1 50       6 @$vals > 0 and not grep {!looks_like_number($_)} @$vals}
  2         9  
617 1 50       6 or croak "-not_in : needs an arrayref of numbers";
618             }
619              
620 49         278 return $self;
621             }
622              
623             sub _inspect {
624 294     294   500 my ($self, $data) = @_;
625              
626 294 100       881 looks_like_number($data)
627             or return $self->msg(INVALID => $data);
628              
629 187 100       421 if (defined $self->{-min}) {
630             $data >= $self->{-min}
631 31 100       74 or return $self->msg(TOO_SMALL => $self->{-min});
632             }
633 181 100       358 if (defined $self->{-max}) {
634             $data <= $self->{-max}
635 12 100       39 or return $self->msg(TOO_BIG => $self->{-max});
636             }
637 177 100       344 if (defined $self->{-not_in}) {
638 5 100       7 grep {$data == $_} @{$self->{-not_in}}
  10         32  
  5         13  
639             and return $self->msg(EXCLUSION_SET => $data);
640             }
641              
642 175         545 return;
643             }
644              
645              
646             #======================================================================
647             package Data::Domain::Int;
648             #======================================================================
649 4     4   28 use strict;
  4         8  
  4         95  
650 4     4   26 use warnings;
  4         8  
  4         587  
651              
652             our @ISA = 'Data::Domain::Num';
653              
654             sub _inspect {
655 76     76   137 my ($self, $data) = @_;
656              
657 76 100 66     529 defined($data) and $data =~ /^-?\d+$/
658             or return $self->msg(INVALID => $data);
659 56         213 return $self->SUPER::_inspect($data);
660             }
661              
662              
663             #======================================================================
664             package Data::Domain::Nat;
665             #======================================================================
666 4     4   28 use strict;
  4         9  
  4         111  
667 4     4   21 use warnings;
  4         28  
  4         523  
668              
669             our @ISA = 'Data::Domain::Num';
670              
671             sub _inspect {
672 3     3   5 my ($self, $data) = @_;
673              
674 3 100 66     33 defined($data) and $data =~ /^\d+$/
675             or return $self->msg(INVALID => $data);
676 2         7 return $self->SUPER::_inspect($data);
677             }
678              
679              
680             #======================================================================
681             package Data::Domain::String;
682             #======================================================================
683 4     4   27 use strict;
  4         6  
  4         106  
684 4     4   23 use warnings;
  4         7  
  4         112  
685 4     4   20 use Carp;
  4         6  
  4         1947  
686             our @ISA = 'Data::Domain';
687              
688             sub new {
689 25     25   54 my $class = shift;
690 25         71 my @options = qw/-regex -antiregex
691             -range -min -max
692             -length -min_length -max_length
693             -not_in/;
694 25         70 my $self = Data::Domain::_parse_args(\@_, \@options, -regex => 'scalar');
695 25         54 bless $self, $class;
696              
697 25         76 $self->_expand_range(qw/-range -min -max/);
698 25         68 $self->_check_min_max(qw/-min -max le/);
699              
700 25         68 $self->_expand_range(qw/-length -min_length -max_length/);
701 25         60 $self->_check_min_max(qw/-min_length -max_length <=/);
702              
703 24         143 return $self;
704             }
705              
706             sub _inspect {
707 162     162   271 my ($self, $data) = @_;
708              
709             # $data must be Unref or obj with a stringification method
710 162 100 100     380 !ref($data) || overload::Method($data, '""')
711             or return $self->msg(INVALID => $data);
712 159 100       340 if ($self->{-min_length}) {
713             length($data) >= $self->{-min_length}
714 6 100       19 or return $self->msg(TOO_SHORT => $self->{-min_length});
715             }
716 158 100       300 if (defined $self->{-max_length}) {
717             length($data) <= $self->{-max_length}
718 5 100       17 or return $self->msg(TOO_LONG => $self->{-max_length});
719             }
720 155 100       319 if ($self->{-regex}) {
721             $data =~ $self->{-regex}
722 132 100       815 or return $self->msg(SHOULD_MATCH => $self->{-regex});
723             }
724 142 100       308 if ($self->{-antiregex}) {
725             $data !~ $self->{-antiregex}
726 2 100       20 or return $self->msg(SHOULD_NOT_MATCH => $self->{-antiregex});
727             }
728 141 100       249 if (defined $self->{-min}) {
729             $data ge $self->{-min}
730 4 100       13 or return $self->msg(TOO_SMALL => $self->{-min});
731             }
732 140 100       255 if (defined $self->{-max}) {
733             $data le $self->{-max}
734 3 100       10 or return $self->msg(TOO_BIG => $self->{-max});
735             }
736 139 100       279 if ($self->{-not_in}) {
737 1 50       3 grep {$data eq $_} @{$self->{-not_in}}
  2         7  
  1         4  
738             and return $self->msg(EXCLUSION_SET => $data);
739             }
740              
741 139         309 return;
742             }
743              
744              
745             #======================================================================
746             package Data::Domain::Date;
747             #======================================================================
748 4     4   26 use strict;
  4         8  
  4         93  
749 4     4   27 use warnings;
  4         8  
  4         105  
750 4     4   19 use Carp;
  4         7  
  4         218  
751 4     4   22 use Try::Tiny;
  4         8  
  4         310  
752             our @ISA = 'Data::Domain';
753              
754              
755 4         25 use autouse 'Date::Calc' => qw/Decode_Date_EU Decode_Date_US Date_to_Text
756 4     4   1899 Delta_Days Add_Delta_Days Today check_date/;
  4         2985  
757              
758             my $date_parser = \&Decode_Date_EU;
759              
760             #----------------------------------------------------------------------
761             # utility functions
762             #----------------------------------------------------------------------
763             sub _print_date {
764 3     3   6 my $date = shift;
765 3         8 $date = _expand_dynamic_date($date);
766 3         14 return Date_to_Text(@$date);
767             }
768              
769              
770             my $dynamic_date = qr/^(today|yesterday|tomorrow)$/;
771              
772             sub _expand_dynamic_date {
773 42     42   56 my $date = shift;
774 42 100       82 if (not ref $date) {
775             $date = {
776             today => [Today],
777             yesterday => [Add_Delta_Days(Today, -1)],
778             tomorrow => [Add_Delta_Days(Today, +1)]
779 7 50       280 }->{$date} or croak "unexpected date : $date";
780             }
781 42         215 return $date;
782             }
783              
784             sub _date_cmp {
785 15     15   30 my ($d1, $d2) = map {_expand_dynamic_date($_)} @_;
  30         45  
786 15         79 return -Delta_Days(@$d1, @$d2);
787             }
788              
789              
790             #----------------------------------------------------------------------
791             # public API
792             #----------------------------------------------------------------------
793              
794             sub parser {
795 1     1   440 my ($class, $new_parser) = @_;
796 1 50       4 not ref $class or croak "Data::Domain::Date::parser is a class method";
797              
798             $date_parser =
799             (ref $new_parser eq 'CODE')
800             ? $new_parser
801             : {US => \&Decode_Date_US,
802 1 50       12 EU => \&Decode_Date_EU}->{$new_parser}
    50          
803             or croak "unknown date parser : $new_parser";
804 1         3 return $date_parser;
805             }
806              
807              
808             sub new {
809 11     11   3621 my $class = shift;
810 11         30 my @options = qw/-range -min -max -not_in/;
811 11         29 my $self = Data::Domain::_parse_args(\@_, \@options);
812 11         23 bless $self, $class;
813              
814 11         54 $self->_expand_range(qw/-range -min -max/);
815              
816             # parse date boundaries into internal representation (arrayrefs)
817 11         23 for my $bound (qw/-min -max/) {
818 21 100 100     97 if ($self->{$bound} and $self->{$bound} !~ $dynamic_date) {
819 6 100       22 my @date = $date_parser->($self->{$bound})
820             or croak "invalid date ($bound): $self->{$bound}";
821 5         84 $self->{$bound} = \@date;
822             }
823             }
824              
825             # check order of boundaries
826 10     2   50 $self->_check_min_max(qw/-min -max/, sub {_date_cmp($_[0], $_[1]) <= 0});
  2         7  
827              
828             # parse dates in the exclusion set into internal representation
829 9 100       95 if ($self->{-not_in}) {
830 1         3 my @excl_dates;
831             try {
832 1     1   28 foreach my $date (@{$self->{-not_in}}) {
  1         4  
833 2 100       13 if ($date =~ $dynamic_date) {
834 1         3 push @excl_dates, $date;
835             }
836             else {
837 1 50       4 my @parsed_date = $date_parser->($date) or die "wrong date";
838 1         17 push @excl_dates, \@parsed_date;
839             }
840             }
841 1         5 @excl_dates > 0;
842             }
843 1 50       6 or croak "-not_in : needs an arrayref of dates";
844 1         18 $self->{-not_in} = \@excl_dates;
845             }
846              
847 9         50 return $self;
848             }
849              
850              
851             sub _inspect {
852 18     18   39 my ($self, $data) = @_;
853              
854 18     18   85 my @date = try {$date_parser->($data)};
  18         483  
855 18 100 66     12925 @date && check_date(@date)
856             or return $self->msg(INVALID => $data);
857              
858 14 100       155 if (defined $self->{-min}) {
859 6         16 my $min = _expand_dynamic_date($self->{-min});
860             !check_date(@$min) || (_date_cmp(\@date, $min) < 0)
861 6 100 66     26 and return $self->msg(TOO_SMALL => _print_date($self->{-min}));
862             }
863              
864 12 100       79 if (defined $self->{-max}) {
865 3         8 my $max = _expand_dynamic_date($self->{-max});
866             !check_date(@$max) || (_date_cmp(\@date, $max) > 0)
867 3 100 66     16 and return $self->msg(TOO_BIG => _print_date($self->{-max}));
868             }
869              
870 11 100       29 if ($self->{-not_in}) {
871 2 100       3 grep {_date_cmp(\@date, $_) == 0} @{$self->{-not_in}}
  4         8  
  2         6  
872             and return $self->msg(EXCLUSION_SET => $data);
873             }
874              
875 10         38 return;
876             }
877              
878              
879             #======================================================================
880             package Data::Domain::Time;
881             #======================================================================
882 4     4   4698 use strict;
  4         10  
  4         137  
883 4     4   22 use warnings;
  4         8  
  4         131  
884 4     4   23 use Carp;
  4         16  
  4         3049  
885             our @ISA = 'Data::Domain';
886              
887             my $time_regex = qr/^(\d\d?):?(\d\d?)?:?(\d\d?)?$/;
888              
889             sub _valid_time {
890 9     9   21 my ($h, $m, $s) = @_;
891 9   50     17 $m ||= 0;
892 9   50     35 $s ||= 0;
893 9   66     56 return ($h <= 23 && $m <= 59 && $s <= 59);
894             }
895              
896              
897             sub _expand_dynamic_time {
898 16     16   21 my $time = shift;
899 16 50       31 if (not ref $time) {
900 0 0       0 $time eq 'now' or croak "unexpected time : $time";
901 0         0 $time = [(localtime)[2, 1, 0]];
902             }
903 16         31 return $time;
904             }
905              
906              
907             sub _time_cmp {
908 7     7   14 my ($t1, $t2) = map {_expand_dynamic_time($_)} @_;
  14         20  
909              
910 7   33     40 return $t1->[0] <=> $t2->[0] # hours
911             || ($t1->[1] || 0) <=> ($t2->[1] || 0) # minutes
912             || ($t1->[2] || 0) <=> ($t2->[2] || 0); # seconds
913             }
914              
915             sub _print_time {
916 2     2   5 my $time = _expand_dynamic_time(shift);
917 2 100       4 return sprintf "%02d:%02d:%02d", map {$_ || 0} @$time;
  6         26  
918             }
919              
920              
921             sub new {
922 3     3   8 my $class = shift;
923 3         9 my @options = qw/-range -min -max/;
924 3         8 my $self = Data::Domain::_parse_args(\@_, \@options);
925 3         7 bless $self, $class;
926              
927 3         11 $self->_expand_range(qw/-range -min -max/);
928              
929             # parse time boundaries
930 3         7 for my $bound (qw/-min -max/) {
931 6 100 66     25 if ($self->{$bound} and $self->{$bound} ne 'now') {
932 4         42 my @time = ($self->{$bound} =~ $time_regex);
933 4 50 33     15 @time && _valid_time(@time)
934             or croak "invalid time ($bound): $self->{$bound}";
935 4         12 $self->{$bound} = \@time;
936             }
937             }
938              
939             # check order of boundaries
940 3     2   18 $self->_check_min_max(qw/-min -max/, sub {_time_cmp($_[0], $_[1]) <= 0});
  2         7  
941              
942 2         18 return $self;
943             }
944              
945              
946             sub _inspect {
947 6     6   11 my ($self, $data) = @_;
948              
949 6         51 my @t = ($data =~ $time_regex);
950 6 100 100     29 @t and _valid_time(@t)
951             or return $self->msg(INVALID => $data);
952              
953 4 100       14 if (defined $self->{-min}) {
954             _time_cmp(\@t, $self->{-min}) < 0
955 3 100       11 and return $self->msg(TOO_SMALL => _print_time($self->{-min}));
956             }
957              
958 3 100       12 if (defined $self->{-max}) {
959             _time_cmp(\@t, $self->{-max}) > 0
960 2 100       4 and return $self->msg(TOO_BIG => _print_time($self->{-max}));
961             }
962              
963 2         10 return;
964             }
965              
966              
967              
968             #======================================================================
969             package Data::Domain::Handle;
970             #======================================================================
971 4     4   31 use strict;
  4         10  
  4         100  
972 4     4   20 use warnings;
  4         8  
  4         125  
973 4     4   22 use Carp;
  4         7  
  4         798  
974             our @ISA = 'Data::Domain';
975              
976             sub new {
977 1     1   3 my $class = shift;
978 1         4 my @options = ();
979 1         3 my $self = Data::Domain::_parse_args(\@_, \@options);
980 1         4 bless $self, $class;
981             }
982              
983             sub _inspect {
984 3     3   8 my ($self, $data) = @_;
985 3 100       18 Scalar::Util::openhandle($data)
986             or return $self->msg(INVALID => '');
987              
988 2         9 return; # otherwise OK, no error
989             }
990              
991              
992              
993              
994             #======================================================================
995             package Data::Domain::Enum;
996             #======================================================================
997 4     4   29 use strict;
  4         7  
  4         101  
998 4     4   23 use warnings;
  4         7  
  4         123  
999 4     4   22 use Carp;
  4         7  
  4         229  
1000 4     4   25 use Try::Tiny;
  4         7  
  4         1195  
1001             our @ISA = 'Data::Domain';
1002              
1003             sub new {
1004 5     5   12 my $class = shift;
1005 5         10 my @options = qw/-values/;
1006 5         17 my $self = Data::Domain::_parse_args(\@_, \@options, -values => 'arrayref');
1007 5         12 bless $self, $class;
1008              
1009 5 50   5   28 try {@{$self->{-values}}} or croak "Enum : incorrect set of values";
  5         124  
  5         47  
1010              
1011 5 100       66 not grep {! defined $_} @{$self->{-values}}
  19         162  
  5         12  
1012             or croak "Enum : undefined element in values";
1013              
1014 4         20 return $self;
1015             }
1016              
1017              
1018             sub _inspect {
1019 6     6   14 my ($self, $data) = @_;
1020              
1021             return $self->msg(NOT_IN_LIST => $data)
1022 6 100       9 if not grep {$_ eq $data} @{$self->{-values}};
  22         53  
  6         13  
1023              
1024 4         10 return; # otherwise OK, no error
1025             }
1026              
1027              
1028             #======================================================================
1029             package Data::Domain::List;
1030             #======================================================================
1031 4     4   29 use strict;
  4         16  
  4         122  
1032 4     4   20 use warnings;
  4         8  
  4         138  
1033 4     4   21 use Carp;
  4         16  
  4         236  
1034 4     4   26 use List::MoreUtils qw/all/;
  4         9  
  4         26  
1035 4     4   4284 use Scalar::Does qw/does/;
  4         10  
  4         20  
1036             our @ISA = 'Data::Domain';
1037              
1038             sub new {
1039 13     13   26 my $class = shift;
1040 13         35 my @options = qw/-items -size -min_size -max_size -any -all/;
1041 13         31 my $self = Data::Domain::_parse_args(\@_, \@options, -items => 'arrayref');
1042 13         28 bless $self, $class;
1043              
1044 13         39 $self->_expand_range(qw/-size -min_size -max_size/);
1045 13         38 $self->_check_min_max(qw/-min_size -max_size <=/);
1046              
1047 12 100       31 if ($self->{-items}) {
1048 5 50       19 does($self->{-items}, 'ARRAY')
1049             or croak "invalid -items for Data::Domain::List";
1050              
1051             # if -items is given, then both -{min,max}_size cannot be shorter
1052 5         109 for my $bound (qw/-min_size -max_size/) {
1053             croak "$bound does not match -items"
1054 10 50 33     27 if $self->{$bound} and $self->{$bound} < @{$self->{-items}};
  0         0  
1055             }
1056             }
1057              
1058             # check that -all or -any are domains or lists of domains
1059 12         25 for my $arg (qw/-all -any/) {
1060 24 100       162 if (my $dom = $self->{$arg}) {
1061 8 100       448 $dom = [$dom] unless does($dom, 'ARRAY');
1062 8 100   9   529 all {does($_, 'Data::Domain') || does($_, 'CODE')} @$dom
  9 50       40  
1063             or croak "invalid arg to $arg in Data::Domain::List";
1064             }
1065             }
1066              
1067 12         138 return $self;
1068             }
1069              
1070              
1071             sub _inspect {
1072 38     38   68 my ($self, $data, $context) = @_;
1073 4     4   3115 no warnings 'recursion';
  4         8  
  4         2279  
1074              
1075 38 100       95 does($data, 'ARRAY')
1076             or return $self->msg(NOT_A_LIST => $data);
1077              
1078 37 100 100     779 if (defined $self->{-min_size} && @$data < $self->{-min_size}) {
1079 1         4 return $self->msg(TOO_SHORT => $self->{-min_size});
1080             }
1081              
1082 36 100 100     103 if (defined $self->{-max_size} && @$data > $self->{-max_size}) {
1083 1         4 return $self->msg(TOO_LONG => $self->{-max_size});
1084             }
1085              
1086 35 100 100     125 return unless $self->{-items} || $self->{-all} || $self->{-any};
      100        
1087              
1088             # prepare context for calling lazy subdomains
1089 33   100     318 $context ||= {root => $data,
1090             flat => {},
1091             path => []};
1092 33         80 local $context->{list} = $data;
1093              
1094             # initializing some variables
1095 33         50 my @msgs;
1096             my $has_invalid;
1097 33   100     84 my $items = $self->{-items} || [];
1098 33         51 my $n_items = @$items;
1099 33         43 my $n_data = @$data;
1100              
1101             # check the -items conditions
1102 33         81 for (my $i = 0; $i < $n_items; $i++) {
1103 50         71 local $context->{path} = [@{$context->{path}}, $i];
  50         127  
1104 50 50       122 my $subdomain = $self->_build_subdomain($items->[$i], $context)
1105             or next;
1106 50         856 $msgs[$i] = $subdomain->inspect($data->[$i], $context);
1107 50   100     246 $has_invalid ||= $msgs[$i];
1108             }
1109              
1110             # check the -all condition (can be a single domain or an arrayref of domains)
1111 33 100       82 if (my $all = $self->{-all}) {
1112 8 50       114 $all = [$all] unless does($all, 'ARRAY');
1113 8         448 my $n_all = @$all;
1114 8         23 for (my $i = $n_items, my $j = 0; # $i iterates over @$data, $j over @$all
1115             $i < $n_data;
1116             $i++, $j = ($j + 1) % $n_all) {
1117 28         45 local $context->{path} = [@{$context->{path}}, $i];
  28         83  
1118 28         64 my $subdomain = $self->_build_subdomain($all->[$j], $context);
1119 28         69 $msgs[$i] = $subdomain->inspect($data->[$i], $context);
1120 28   100     176 $has_invalid ||= $msgs[$i];
1121             }
1122             }
1123              
1124             # stop here if there was any error message
1125 33 100       112 return \@msgs if $has_invalid;
1126              
1127             # all other conditions were good, now check the "any" conditions
1128 24 100       53 if (my $any = $self->{-any}) {
1129 13 100       138 $any = [$any] unless does($any, 'ARRAY');
1130              
1131             # there must be data to inspect
1132             $n_data > $n_items
1133 13 100 33     752 or return $self->msg(ANY => ($any->[0]{-name} || $any->[0]->subclass));
1134              
1135             # inspect the remaining data for all 'any' conditions
1136             CONDITION:
1137 12         21 foreach my $condition (@$any) {
1138 15         24 my $subdomain;
1139 15         36 for (my $i = $n_items; $i < $n_data; $i++) {
1140 31         41 local $context->{path} = [@{$context->{path}}, $i];
  31         73  
1141 31         66 $subdomain = $self->_build_subdomain($condition, $context);
1142 31         70 my $error = $subdomain->inspect($data->[$i], $context);
1143 31 100       109 next CONDITION if not $error;
1144             }
1145 4   33     13 return $self->msg(ANY => ($subdomain->{-name} || $subdomain->subclass));
1146             }
1147             }
1148              
1149 19         118 return; # OK, no error
1150             }
1151              
1152              
1153             #======================================================================
1154             package Data::Domain::Struct;
1155             #======================================================================
1156 4     4   48 use strict;
  4         8  
  4         102  
1157 4     4   20 use warnings;
  4         8  
  4         140  
1158 4     4   38 use Carp;
  4         9  
  4         248  
1159 4     4   24 use Scalar::Does qw/does/;
  4         16  
  4         25  
1160             our @ISA = 'Data::Domain';
1161              
1162             sub new {
1163 14     14   31 my $class = shift;
1164 14         34 my @options = qw/-fields -exclude -keys -values/;
1165 14         36 my $self = Data::Domain::_parse_args(\@_, \@options, -fields => 'arrayref');
1166 14         30 bless $self, $class;
1167              
1168 14   100     112 my $fields = $self->{-fields} || [];
1169 14 100       44 if (does($fields, 'ARRAY')) {
    50          
1170             # transform arrayref into hashref plus an ordered list of keys
1171 13         826 $self->{-fields_list} = [];
1172 13         29 $self->{-fields} = {};
1173 13         42 for (my $i = 0; $i < @$fields; $i += 2) {
1174 22         56 my ($key, $val) = ($fields->[$i], $fields->[$i+1]);
1175 22         27 push @{$self->{-fields_list}}, $key;
  22         46  
1176 22         68 $self->{-fields}{$key} = $val;
1177             }
1178             }
1179             elsif (does($fields, 'HASH')) {
1180             # keep given hashref, add list of keys
1181 1         46 $self->{-fields_list} = [keys %$fields];
1182             }
1183             else {
1184 0         0 croak "invalid data for -fields option";
1185             }
1186              
1187             # check that -exclude is an arrayref or a regex or a string
1188 14 100       43 if (my $exclude = $self->{-exclude}) {
1189 3 50 100     7 does($exclude, 'ARRAY') || does($exclude, 'Regexp') || !ref($exclude)
      66        
1190             or croak "invalid data for -exclude option";
1191             }
1192              
1193              
1194             # check that -keys or -values are List domains
1195 14         203 for my $arg (qw/-keys -values/) {
1196 28 100       90 if (my $dom = $self->{$arg}) {
1197 2 50 33     43 does($dom, 'Data::Domain::List') or does($dom, 'CODE')
1198             or croak "$arg in Data::Domain::Struct should be a List domain";
1199             }
1200             }
1201              
1202 14         94 return $self;
1203             }
1204              
1205              
1206             sub _inspect {
1207 137     137   274 my ($self, $data, $context) = @_;
1208 4     4   2958 no warnings 'recursion';
  4         10  
  4         1796  
1209              
1210             # check that $data is a hashref
1211 137 100       352 does($data, 'HASH')
1212             or return $self->msg(NOT_A_HASH => $data);
1213              
1214             # check if there are any forbidden fields
1215 135 100       3484 if (my $exclude = $self->{-exclude}) {
1216             FIELD:
1217 9         29 foreach my $field (keys %$data) {
1218 15 100       107 next FIELD if $self->{-fields}{$field};
1219              
1220 8 100 100     57 return $self->msg(FORBIDDEN_FIELD => $field)
1221             if match::simple::match($field, $exclude)
1222             or match::simple::match($exclude, ['*', 'all']);
1223             }
1224             }
1225              
1226 129         255 my %msgs;
1227             my $has_invalid;
1228              
1229             # prepare context for calling lazy subdomains
1230 129   100     391 $context ||= {root => $data,
1231             flat => {},
1232             list => [],
1233             path => []};
1234 129         171 local $context->{flat} = {%{$context->{flat}}, %$data};
  129         853  
1235              
1236             # check fields of the domain
1237 129         249 foreach my $field (@{$self->{-fields_list}}) {
  129         253  
1238 354         485 local $context->{path} = [@{$context->{path}}, $field];
  354         3180  
1239 354         714 my $field_spec = $self->{-fields}{$field};
1240 354         709 my $subdomain = $self->_build_subdomain($field_spec, $context);
1241 353         1471 my $msg = $subdomain->inspect($data->{$field}, $context);
1242 254 100       525 $msgs{$field} = $msg if $msg;
1243 254   100     1377 $has_invalid ||= $msg;
1244             }
1245              
1246             # check the List domain for keys
1247 29 100       73 if (my $keys_dom = $self->{-keys}) {
1248 3         66 local $context->{path} = [@{$context->{path}}, "-keys"];
  3         10  
1249 3         10 my $subdomain = $self->_build_subdomain($keys_dom, $context);
1250 3 100       11 $msgs{-keys} = $subdomain->inspect([keys %$data], $context)
1251             and $has_invalid = 1;
1252             }
1253              
1254             # check the List domain for values
1255 29 100       92 if (my $values_dom = $self->{-values}) {
1256 3         52 local $context->{path} = [@{$context->{path}}, "-values"];
  3         9  
1257 3         8 my $subdomain = $self->_build_subdomain($values_dom, $context);
1258 3 100       11 $msgs{-values} = $subdomain->inspect([values %$data], $context)
1259             and $has_invalid = 1;
1260             }
1261              
1262 29 100       226 return $has_invalid ? \%msgs : undef;
1263             }
1264              
1265             #======================================================================
1266             package Data::Domain::One_of;
1267             #======================================================================
1268 4     4   29 use strict;
  4         9  
  4         261  
1269 4     4   25 use warnings;
  4         7  
  4         138  
1270 4     4   73 use Carp;
  4         12  
  4         795  
1271             our @ISA = 'Data::Domain';
1272              
1273             sub new {
1274 2     2   4 my $class = shift;
1275 2         6 my @options = qw/-options/;
1276 2         8 my $self = Data::Domain::_parse_args(\@_, \@options, -options => 'arrayref');
1277 2         5 bless $self, $class;
1278              
1279 2 50       42 Scalar::Does::does($self->{-options}, 'ARRAY')
1280             or croak "One_of: invalid options";
1281              
1282 2         51 return $self;
1283             }
1284              
1285              
1286             sub _inspect {
1287 213     213   366 my ($self, $data, $context) = @_;
1288 213         293 my @msgs;
1289 4     4   29 no warnings 'recursion';
  4         7  
  4         552  
1290              
1291 213         280 for my $subdomain (@{$self->{-options}}) {
  213         449  
1292 321 100       1265 my $msg = $subdomain->inspect($data, $context)
1293             or return; # $subdomain was successful
1294 112         283 push @msgs, $msg;
1295             }
1296 4         21 return \@msgs;
1297             }
1298              
1299              
1300             #======================================================================
1301             package Data::Domain::All_of;
1302             #======================================================================
1303 4     4   34 use strict;
  4         10  
  4         132  
1304 4     4   23 use warnings;
  4         6  
  4         131  
1305 4     4   21 use Carp;
  4         7  
  4         799  
1306             our @ISA = 'Data::Domain';
1307              
1308             sub new {
1309 1     1   2 my $class = shift;
1310 1         3 my @options = qw/-options/;
1311 1         4 my $self = Data::Domain::_parse_args(\@_, \@options, -options => 'arrayref');
1312 1         4 bless $self, $class;
1313              
1314 1 50       36 Scalar::Does::does($self->{-options}, 'ARRAY')
1315             or croak "All_of: invalid options";
1316              
1317 1         31 return $self;
1318             }
1319              
1320              
1321             sub _inspect {
1322 3     3   6 my ($self, $data, $context) = @_;
1323 3         7 my @msgs;
1324 4     4   30 no warnings 'recursion';
  4         8  
  4         534  
1325              
1326 3         5 for my $subdomain (@{$self->{-options}}) {
  3         7  
1327 6         14 my $msg = $subdomain->inspect($data, $context);
1328 6 100       19 push @msgs, $msg if $msg; # subdomain failed
1329             }
1330 3 100       18 return @msgs ? \@msgs : undef;
1331             }
1332              
1333              
1334             #======================================================================
1335             1;
1336              
1337              
1338             __END__