File Coverage

blib/lib/Data/Domain.pm
Criterion Covered Total %
statement 664 707 93.9
branch 279 342 81.5
condition 102 149 68.4
subroutine 138 146 94.5
pod 5 5 100.0
total 1188 1349 88.0


line stmt bran cond sub pod time code
1             #======================================================================
2             package Data::Domain; # documentation at end of file
3             #======================================================================
4 4     4   435346 use 5.010;
  4         47  
5 4     4   23 use strict;
  4         7  
  4         91  
6 4     4   19 use warnings;
  4         8  
  4         110  
7 4     4   30 use Carp;
  4         8  
  4         232  
8 4     4   1954 use Data::Dumper;
  4         20759  
  4         221  
9 4     4   1989 use Scalar::Does 0.007;
  4         488731  
  4         52  
10 4     4   2417 use Scalar::Util ();
  4         9  
  4         69  
11 4     4   22 use Try::Tiny;
  4         9  
  4         264  
12 4     4   2317 use List::MoreUtils qw/part natatime any/;
  4         41409  
  4         26  
13 4     4   4975 use if $] < 5.037, experimental => 'smartmatch'; # smartmatch no longer experimental after 5.037
  4         8  
  4         40  
14 4 50       101 use overload '""' => \&_stringify,
15 4     4   18060 $] < 5.037 ? ('~~' => \&_matches) : (); # fully deprecated, so cannot be overloaded
  4         20  
16 4     4   2635 use match::simple ();
  4         8520  
  4         599  
17              
18             our $VERSION = "1.12";
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   23 @CONSTRUCTORS = qw/Whatever Empty
33             Num Int Nat Date Time String Handle
34             Enum List Struct One_of All_of/;
35 4         506 %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         103 (map {$_ => \&_wrap_domain } @CONSTRUCTORS ),
54 4         21 (map {$_ => \&_wrap_shortcut_options} keys %SHORTCUTS) ],
  44         131  
55             groups => { constructors => \@CONSTRUCTORS,
56             shortcuts => [keys %SHORTCUTS] },
57             collectors => { INIT => \&_sub_exporter_init },
58             installer => \&_sub_exporter_installer,
59 4     4   2796 };
  4         47288  
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   477 my ($collection, $context) = @_;
68 4         8 my $args = $context->{import_args};
69             my ($exclude, $regular_args)
70 4 100 66 5   40 = part {!ref $_->[0] && $_->[0] =~ /^!/ ? 0 : 1} @$args;
  5         63  
71 4         21 @$args = @$regular_args;
72 4         9 @dont_export = map {substr($_->[0], 1)} @$exclude;
  1         5  
73 4         14 1;
74             }
75              
76             # install symbols, except those that belong to @dont_export
77             sub _sub_exporter_installer {
78 4     4   35 my ($arg, $to_export) = @_;
79 4         67 my %export_hash = @$to_export;
80 4         17 delete @export_hash{@dont_export};
81 4         81 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   2750 my ($class, $name, $args, $coll) = @_;
90 56     131   283 return sub {return "Data::Domain::$name"->new(@_)};
  131         66589  
91             }
92              
93              
94             # # shortcuts group : calling 'Whatever' with various pre-built options
95             sub _wrap_shortcut_options {
96 44     44   1395 my ($class, $name, $args, $coll) = @_;
97 44     14   169 return sub {return Data::Domain::Whatever->new(@{$SHORTCUTS{$name}}, @_)};
  14         6664  
  14         78  
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): %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(s) champ(s) interdit(s): %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 7315 my ($class, $new_messages) = @_;
211 3 50 33     22 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       20 : $builtin_msgs->{$new_messages}
    50          
216             or croak "no such builtin messages ($new_messages)";
217             }
218              
219              
220             sub inspect {
221 990     990 1 3602 my ($self, $data, $context) = @_;
222 4     4   7907 no warnings 'recursion';
  4         22  
  4         6121  
223              
224 990 100       2069 if (!defined $data) {
225             # success if data was optional;
226 33 100       114 return if $self->{-optional};
227              
228             # only the 'Whatever' domain can accept undef; other domains will fail
229 26 100       175 return $self->msg(UNDEFINED => '')
230             unless $self->isa("Data::Domain::Whatever");
231             }
232             else { # if $data is defined
233             # check some general properties
234 957 100       2099 if (my $isa = $self->{-isa}) {
235 2     2   74 try {$data->isa($isa)}
236 2 100       12 or return $self->msg(MATCH_ISA => $isa);
237             }
238 956 100       1712 if (my $role = $self->{-does}) {
239 4 100       16 does($data, $role)
240             or return $self->msg(MATCH_DOES => $role);
241             }
242 954 100       2285 if (my $can = $self->{-can}) {
243 3 100       9 $can = [$can] unless does($can, 'ARRAY');
244 3         948 foreach my $method (@$can) {
245 5     5   136 try {$data->can($method)}
246 5 100       38 or return $self->msg(MATCH_CAN => $method);
247             }
248             }
249 953 100       1635 if (my $match_target = $self->{-matches}) {
250 2 100       17 match::simple::match($data, $match_target)
251             or return $self->msg(MATCH_SMART => $match_target);
252             }
253 952 100       1646 if ($self->{-has}) {
254             # EXPERIMENTAL: check methods results
255 1         6 my @msgs = $self->_check_has($data, $context);
256 1 50       8 return {HAS => \@msgs} if @msgs;
257             }
258 951 100       1665 if (defined $self->{-blessed}) {
259             return $self->msg(MATCH_BLESSED => $self->{-blessed})
260 8 100 100     83 if Scalar::Util::blessed($data) xor $self->{-blessed};
261             }
262 947 100       1580 if (defined $self->{-package}) {
263             return $self->msg(MATCH_PACKAGE => $self->{-package})
264 3 100 100     42 if (!ref($data) && $data->isa($data)) xor $self->{-package};
      50        
265             }
266 945 50       1602 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 945 50       1508 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 945 50       1676 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 954 100       1696 if (defined $self->{-true}) {
282             return $self->msg(MATCH_TRUE => $self->{-true})
283 11 100 100     71 if $data xor $self->{-true};
284             }
285 949 100       1504 if (defined $self->{-ref}) {
286             return $self->msg(MATCH_REF => $self->{-ref})
287 6 100 100     32 if ref $data xor $self->{-ref};
288             }
289              
290             # now call domain-specific _inspect()
291 946         2778 return $self->_inspect($data, $context)
292             }
293              
294              
295             sub _check_has {
296 1     1   4 my ($self, $data, $context) = @_;
297              
298 1         1 my @msgs;
299 1         2 my $iterator = natatime 2, @{$self->{-has}};
  1         15  
300 1         40 while (my ($meth_to_call, $expectation) = $iterator->()) {
301 3 100       16 my ($meth, @args) = does($meth_to_call, 'ARRAY') ? @$meth_to_call
302             : ($meth_to_call);
303 3         1008 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   170 $msg = try {my $result = $data->$meth(@args);
312 2         51 $expectation->inspect($result)}
313 3     1   277 catch {(my $error_msg = $_) =~ s/\bat\b.*//s; $error_msg};
  1         25  
  1         5  
314             }
315 3 100       62 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 242     242 1 2131 my ($self, $msg_id, @args) = @_;
355 242         451 my $msgs = $self->{-messages};
356 242         467 my $subclass = $self->subclass;
357 242   66     851 my $name = $self->{-name} || $subclass;
358 242         310 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   37 no if $] ge '5.022000', warnings => 'redundant';
  4         12  
  4         37  
363              
364             # if there is a user_defined message, return it
365 242 100       460 if (defined $msgs) {
366 11         47 for (ref $msgs) {
367 11 100       36 /^CODE/ and return $msgs->($msg_id, @args); # user function
368 10 100       101 /^$/ and return "$name: $msgs"; # user constant string
369 2 50       17 /^HASH/ and do { $msg = $msgs->{$msg_id} # user hash of msgs
  2 50       26  
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 231 100       517 return $global_msgs->($msg_id, @args) if ref $global_msgs eq 'CODE';
379             $msg = $global_msgs->{$subclass}{$msg_id} # otherwise
380 230 50 66     887 || $global_msgs->{Generic}{$msg_id}
381             or croak "no error string for message $msg_id";
382              
383 230         1411 return sprintf "$name: $msg", @args;
384             }
385              
386              
387             sub subclass { # returns the class name without initial 'Data::Domain::'
388 369     369 1 549 my ($self) = @_;
389 369   33     798 my $class = ref($self) || $self;
390 369         1484 (my $subclass = $class) =~ s/^Data::Domain:://;
391 369         1860 return $subclass;
392             }
393              
394              
395             sub _expand_range {
396 127     127   259 my ($self, $range_field, $min_field, $max_field) = @_;
397 127   66     796 my $name = $self->{-name} || $self->subclass;
398              
399             # the range field will be replaced by min and max fields
400 127 100       360 if (my $range = delete $self->{$range_field}) {
401 13         25 for ($min_field, $max_field) {
402 26 50       60 not defined $self->{$_}
403             or croak "$name: incompatible options: $range_field / $_";
404             }
405 13 50 33     69 does($range, 'ARRAY') and @$range == 2
406             or croak "$name: invalid argument for $range";
407 13         332 @{$self}{$min_field, $max_field} = @$range;
  13         50  
408             }
409             }
410              
411              
412             sub _check_min_max {
413 126     126   255 my ($self, $min_field, $max_field, $cmp_func) = @_;
414              
415             # choose the appropriate comparison function
416 126 100   13   357 if ($cmp_func eq '<=') {$cmp_func = sub {$_[0] <= $_[1]}}
  88 100       263  
  13 50       53  
417 25     4   86 elsif ($cmp_func eq 'le') {$cmp_func = sub {$_[0] le $_[1]}}
  4         19  
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         2277 my ($min, $max) = @{$self}{$min_field, $max_field};
  126         268  
423 126 100 100     485 if (defined $min && defined $max) {
424 21 100       46 $cmp_func->($min, $max)
425             or croak $self->subclass . ": incompatible min/max values ($min/$max)";
426             }
427             }
428              
429              
430             sub _build_subdomain {
431 476     476   843 my ($self, $domain, $context) = @_;
432 4     4   3315 no warnings 'recursion';
  4         8  
  4         3463  
433              
434             # avoid infinite loop
435 476 100       607 @{$context->{path}} < $MAX_DEEP
  476         3967  
436             or croak "inspect() deepness exceeded $MAX_DEEP; "
437             . "modify \$Data::Domain::MAX_DEEP if you need more";
438              
439 475 100       1149 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   9211 $domain = try {$domain->($context)}
445             catch { # remove "at source_file, line ..." from error message
446 1     1   278 (my $error_msg = $_) =~ s/\bat\b.*//s;
447             # return an empty domain that reports the error message
448 1         9 Data::Domain::Empty->new(-name => "domain parameters",
449             -messages => $error_msg);
450 230         8555 };
451             # did we really get a domain ?
452 230 50       3224 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       291 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 475         8312 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   292 my ($args_ref, $options_ref, $default_option, $arg_type) = @_;
482              
483 153         214 my %parsed;
484              
485             # parse named arguments
486 153   100     801 while (@$args_ref and $args_ref->[0] =~ /^-/) {
487 123 50   518   655 any {$args_ref->[0] eq $_} (@$options_ref, @common_options)
  518         839  
488             or croak "invalid argument: $args_ref->[0]";
489 123         425 my ($key, $val) = (shift @$args_ref, shift @$args_ref);
490 123         490 $parsed{$key} = $val;
491             }
492              
493             # remaining arguments are mapped to the default option
494 153 100       478 if (@$args_ref) {
495 24 50       54 $default_option or croak "too many args to new()";
496 24 50       54 not exists $parsed{$default_option}
497             or croak "can't have default args if $default_option is set";
498 24 50       100 $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         346 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   1216 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 213     213   2563 my ($self) = @_;
532 213         634 my $dumper = Data::Dumper->new([$self])->Indent(0)->Terse(1);
533 213         7027 return $dumper->Dump;
534             }
535              
536              
537              
538             #======================================================================
539             package Data::Domain::Whatever;
540             #======================================================================
541 4     4   31 use strict;
  4         8  
  4         158  
542 4     4   32 use warnings;
  4         22  
  4         102  
543 4     4   20 use Carp;
  4         38  
  4         257  
544 4     4   98 use Scalar::Does qw/does/;
  4         11  
  4         32  
545             our @ISA = 'Data::Domain';
546              
547             sub new {
548 26     26   47 my $class = shift;
549 26         52 my @options = qw/-defined/;
550 26         63 my $self = Data::Domain::_parse_args( \@_, \@options );
551 26         57 bless $self, $class;
552              
553             not ($self->{-defined } && $self->{-optional})
554 26 50 66     146 or croak "both -defined and -optional: meaningless!";
555              
556 26         106 return $self;
557             }
558              
559             sub _inspect {
560 32     32   72 my ($self, $data) = @_;
561              
562 32 100       69 if (defined $self->{-defined}) {
563             return $self->msg(MATCH_DEFINED => $self->{-defined})
564 9 100 100     72 if defined($data) xor $self->{-defined};
565             }
566              
567             # otherwise, success
568 27         142 return;
569             }
570              
571              
572             #======================================================================
573             package Data::Domain::Empty;
574             #======================================================================
575 4     4   2980 use strict;
  4         32  
  4         87  
576 4     4   19 use warnings;
  4         9  
  4         109  
577 4     4   20 use Carp;
  4         6  
  4         559  
578             our @ISA = 'Data::Domain';
579              
580             sub new {
581 2     2   8 my $class = shift;
582 2         6 my @options = ();
583 2         9 my $self = Data::Domain::_parse_args( \@_, \@options );
584 2         19 bless $self, $class;
585             }
586              
587             sub _inspect {
588 5     5   21 my ($self, $data) = @_;
589              
590 5         15 return $self->msg(INVALID => ''); # always fails
591             }
592              
593              
594             #======================================================================
595             package Data::Domain::Num;
596             #======================================================================
597 4     4   25 use strict;
  4         7  
  4         73  
598 4     4   16 use warnings;
  4         9  
  4         85  
599 4     4   18 use Carp;
  4         13  
  4         187  
600 4     4   25 use Scalar::Util qw/looks_like_number/;
  4         17  
  4         146  
601 4     4   28 use Try::Tiny;
  4         9  
  4         1566  
602              
603             our @ISA = 'Data::Domain';
604              
605             sub new {
606 50     50   84 my $class = shift;
607 50         116 my @options = qw/-range -min -max -not_in/;
608 50         113 my $self = Data::Domain::_parse_args(\@_, \@options);
609 50         93 bless $self, $class;
610              
611 50         156 $self->_expand_range(qw/-range -min -max/);
612 50         135 $self->_check_min_max(qw/-min -max <=/);
613              
614 49 100       140 if ($self->{-not_in}) {
615 1     1   30 try {my $vals = $self->{-not_in};
616 1 50       8 @$vals > 0 and not grep {!looks_like_number($_)} @$vals}
  2         10  
617 1 50       6 or croak "-not_in : needs an arrayref of numbers";
618             }
619              
620 49         345 return $self;
621             }
622              
623             sub _inspect {
624 300     300   524 my ($self, $data) = @_;
625              
626 300 100       949 looks_like_number($data)
627             or return $self->msg(INVALID => $data);
628              
629 193 100       445 if (defined $self->{-min}) {
630             $data >= $self->{-min}
631 31 100       91 or return $self->msg(TOO_SMALL => $self->{-min});
632             }
633 187 100       402 if (defined $self->{-max}) {
634             $data <= $self->{-max}
635 12 100       43 or return $self->msg(TOO_BIG => $self->{-max});
636             }
637 183 100       354 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 181         547 return;
643             }
644              
645              
646             #======================================================================
647             package Data::Domain::Int;
648             #======================================================================
649 4     4   31 use strict;
  4         7  
  4         105  
650 4     4   28 use warnings;
  4         6  
  4         644  
651              
652             our @ISA = 'Data::Domain::Num';
653              
654             sub _inspect {
655 83     83   158 my ($self, $data) = @_;
656              
657 83 100 66     590 defined($data) and $data =~ /^-?\d+$/
658             or return $self->msg(INVALID => $data);
659 62         229 return $self->SUPER::_inspect($data);
660             }
661              
662              
663             #======================================================================
664             package Data::Domain::Nat;
665             #======================================================================
666 4     4   46 use strict;
  4         14  
  4         150  
667 4     4   34 use warnings;
  4         10  
  4         544  
668              
669             our @ISA = 'Data::Domain::Num';
670              
671             sub _inspect {
672 3     3   7 my ($self, $data) = @_;
673              
674 3 100 66     31 defined($data) and $data =~ /^\d+$/
675             or return $self->msg(INVALID => $data);
676 2         8 return $self->SUPER::_inspect($data);
677             }
678              
679              
680             #======================================================================
681             package Data::Domain::String;
682             #======================================================================
683 4     4   36 use strict;
  4         34  
  4         105  
684 4     4   30 use warnings;
  4         5  
  4         150  
685 4     4   21 use Carp;
  4         8  
  4         2005  
686             our @ISA = 'Data::Domain';
687              
688             sub new {
689 25     25   52 my $class = shift;
690 25         68 my @options = qw/-regex -antiregex
691             -range -min -max
692             -length -min_length -max_length
693             -not_in/;
694 25         72 my $self = Data::Domain::_parse_args(\@_, \@options, -regex => 'scalar');
695 25         58 bless $self, $class;
696              
697 25         82 $self->_expand_range(qw/-range -min -max/);
698 25         78 $self->_check_min_max(qw/-min -max le/);
699              
700 25         60 $self->_expand_range(qw/-length -min_length -max_length/);
701 25         62 $self->_check_min_max(qw/-min_length -max_length <=/);
702              
703 24         175 return $self;
704             }
705              
706             sub _inspect {
707 162     162   269 my ($self, $data) = @_;
708              
709             # $data must be Unref or obj with a stringification method
710 162 100 100     400 !ref($data) || overload::Method($data, '""')
711             or return $self->msg(INVALID => $data);
712 159 100       388 if ($self->{-min_length}) {
713             length($data) >= $self->{-min_length}
714 6 100       21 or return $self->msg(TOO_SHORT => $self->{-min_length});
715             }
716 158 100       299 if (defined $self->{-max_length}) {
717             length($data) <= $self->{-max_length}
718 5 100       19 or return $self->msg(TOO_LONG => $self->{-max_length});
719             }
720 155 100       309 if ($self->{-regex}) {
721             $data =~ $self->{-regex}
722 132 100       845 or return $self->msg(SHOULD_MATCH => $self->{-regex});
723             }
724 142 100       290 if ($self->{-antiregex}) {
725             $data !~ $self->{-antiregex}
726 2 100       34 or return $self->msg(SHOULD_NOT_MATCH => $self->{-antiregex});
727             }
728 141 100       283 if (defined $self->{-min}) {
729             $data ge $self->{-min}
730 4 100       17 or return $self->msg(TOO_SMALL => $self->{-min});
731             }
732 140 100       272 if (defined $self->{-max}) {
733             $data le $self->{-max}
734 3 100       19 or return $self->msg(TOO_BIG => $self->{-max});
735             }
736 139 100       258 if ($self->{-not_in}) {
737 1 50       3 grep {$data eq $_} @{$self->{-not_in}}
  2         17  
  1         4  
738             and return $self->msg(EXCLUSION_SET => $data);
739             }
740              
741 139         326 return;
742             }
743              
744              
745             #======================================================================
746             package Data::Domain::Date;
747             #======================================================================
748 4     4   28 use strict;
  4         8  
  4         126  
749 4     4   22 use warnings;
  4         16  
  4         102  
750 4     4   20 use Carp;
  4         28  
  4         230  
751 4     4   25 use Try::Tiny;
  4         6  
  4         329  
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   2165 Delta_Days Add_Delta_Days Today check_date/;
  4         3033  
757              
758             my $date_parser = \&Decode_Date_EU;
759              
760             #----------------------------------------------------------------------
761             # utility functions
762             #----------------------------------------------------------------------
763             sub _print_date {
764 3     3   7 my $date = shift;
765 3         9 $date = _expand_dynamic_date($date);
766 3         17 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   59 my $date = shift;
774 42 100       81 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       303 }->{$date} or croak "unexpected date : $date";
780             }
781 42         223 return $date;
782             }
783              
784             sub _date_cmp {
785 15     15   30 my ($d1, $d2) = map {_expand_dynamic_date($_)} @_;
  30         42  
786 15         87 return -Delta_Days(@$d1, @$d2);
787             }
788              
789              
790             #----------------------------------------------------------------------
791             # public API
792             #----------------------------------------------------------------------
793              
794             sub parser {
795 1     1   470 my ($class, $new_parser) = @_;
796 1 50       5 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       13 EU => \&Decode_Date_EU}->{$new_parser}
    50          
803             or croak "unknown date parser : $new_parser";
804 1         4 return $date_parser;
805             }
806              
807              
808             sub new {
809 11     11   3798 my $class = shift;
810 11         29 my @options = qw/-range -min -max -not_in/;
811 11         31 my $self = Data::Domain::_parse_args(\@_, \@options);
812 11         24 bless $self, $class;
813              
814 11         52 $self->_expand_range(qw/-range -min -max/);
815              
816             # parse date boundaries into internal representation (arrayrefs)
817 11         22 for my $bound (qw/-min -max/) {
818 21 100 100     120 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   56 $self->_check_min_max(qw/-min -max/, sub {_date_cmp($_[0], $_[1]) <= 0});
  2         6  
827              
828             # parse dates in the exclusion set into internal representation
829 9 100       113 if ($self->{-not_in}) {
830 1         3 my @excl_dates;
831             try {
832 1     1   27 foreach my $date (@{$self->{-not_in}}) {
  1         4  
833 2 100       12 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         4 @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         56 return $self;
848             }
849              
850              
851             sub _inspect {
852 18     18   40 my ($self, $data) = @_;
853              
854 18     18   94 my @date = try {$date_parser->($data)};
  18         535  
855 18 100 66     14312 @date && check_date(@date)
856             or return $self->msg(INVALID => $data);
857              
858 14 100       190 if (defined $self->{-min}) {
859 6         17 my $min = _expand_dynamic_date($self->{-min});
860             !check_date(@$min) || (_date_cmp(\@date, $min) < 0)
861 6 100 66     33 and return $self->msg(TOO_SMALL => _print_date($self->{-min}));
862             }
863              
864 12 100       90 if (defined $self->{-max}) {
865 3         9 my $max = _expand_dynamic_date($self->{-max});
866             !check_date(@$max) || (_date_cmp(\@date, $max) > 0)
867 3 100 66     15 and return $self->msg(TOO_BIG => _print_date($self->{-max}));
868             }
869              
870 11 100       32 if ($self->{-not_in}) {
871 2 100       7 grep {_date_cmp(\@date, $_) == 0} @{$self->{-not_in}}
  4         10  
  2         11  
872             and return $self->msg(EXCLUSION_SET => $data);
873             }
874              
875 10         44 return;
876             }
877              
878              
879             #======================================================================
880             package Data::Domain::Time;
881             #======================================================================
882 4     4   4675 use strict;
  4         17  
  4         145  
883 4     4   23 use warnings;
  4         9  
  4         151  
884 4     4   32 use Carp;
  4         21  
  4         3126  
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     20 $m ||= 0;
892 9   50     64 $s ||= 0;
893 9   66     71 return ($h <= 23 && $m <= 59 && $s <= 59);
894             }
895              
896              
897             sub _expand_dynamic_time {
898 16     16   25 my $time = shift;
899 16 50       29 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         34 return $time;
904             }
905              
906              
907             sub _time_cmp {
908 7     7   13 my ($t1, $t2) = map {_expand_dynamic_time($_)} @_;
  14         38  
909              
910 7   33     58 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   6 my $time = _expand_dynamic_time(shift);
917 2 100       5 return sprintf "%02d:%02d:%02d", map {$_ || 0} @$time;
  6         51  
918             }
919              
920              
921             sub new {
922 3     3   6 my $class = shift;
923 3         10 my @options = qw/-range -min -max/;
924 3         9 my $self = Data::Domain::_parse_args(\@_, \@options);
925 3         8 bless $self, $class;
926              
927 3         15 $self->_expand_range(qw/-range -min -max/);
928              
929             # parse time boundaries
930 3         14 for my $bound (qw/-min -max/) {
931 6 100 66     35 if ($self->{$bound} and $self->{$bound} ne 'now') {
932 4         32 my @time = ($self->{$bound} =~ $time_regex);
933 4 50 33     18 @time && _valid_time(@time)
934             or croak "invalid time ($bound): $self->{$bound}";
935 4         13 $self->{$bound} = \@time;
936             }
937             }
938              
939             # check order of boundaries
940 3     2   22 $self->_check_min_max(qw/-min -max/, sub {_time_cmp($_[0], $_[1]) <= 0});
  2         18  
941              
942 2         26 return $self;
943             }
944              
945              
946             sub _inspect {
947 6     6   13 my ($self, $data) = @_;
948              
949 6         53 my @t = ($data =~ $time_regex);
950 6 100 100     32 @t and _valid_time(@t)
951             or return $self->msg(INVALID => $data);
952              
953 4 100       15 if (defined $self->{-min}) {
954             _time_cmp(\@t, $self->{-min}) < 0
955 3 100       16 and return $self->msg(TOO_SMALL => _print_time($self->{-min}));
956             }
957              
958 3 100       7 if (defined $self->{-max}) {
959             _time_cmp(\@t, $self->{-max}) > 0
960 2 100       12 and return $self->msg(TOO_BIG => _print_time($self->{-max}));
961             }
962              
963 2         12 return;
964             }
965              
966              
967              
968             #======================================================================
969             package Data::Domain::Handle;
970             #======================================================================
971 4     4   43 use strict;
  4         13  
  4         112  
972 4     4   21 use warnings;
  4         6  
  4         142  
973 4     4   23 use Carp;
  4         6  
  4         822  
974             our @ISA = 'Data::Domain';
975              
976             sub new {
977 1     1   4 my $class = shift;
978 1         3 my @options = ();
979 1         4 my $self = Data::Domain::_parse_args(\@_, \@options);
980 1         5 bless $self, $class;
981             }
982              
983             sub _inspect {
984 3     3   15 my ($self, $data) = @_;
985 3 100       19 Scalar::Util::openhandle($data)
986             or return $self->msg(INVALID => '');
987              
988 2         11 return; # otherwise OK, no error
989             }
990              
991              
992              
993              
994             #======================================================================
995             package Data::Domain::Enum;
996             #======================================================================
997 4     4   29 use strict;
  4         6  
  4         95  
998 4     4   19 use warnings;
  4         15  
  4         146  
999 4     4   23 use Carp;
  4         10  
  4         239  
1000 4     4   26 use Try::Tiny;
  4         7  
  4         1152  
1001             our @ISA = 'Data::Domain';
1002              
1003             sub new {
1004 5     5   9 my $class = shift;
1005 5         12 my @options = qw/-values/;
1006 5         14 my $self = Data::Domain::_parse_args(\@_, \@options, -values => 'arrayref');
1007 5         12 bless $self, $class;
1008              
1009 5 50   5   36 try {@{$self->{-values}}} or croak "Enum : incorrect set of values";
  5         128  
  5         58  
1010              
1011 5 100       66 not grep {! defined $_} @{$self->{-values}}
  19         169  
  5         12  
1012             or croak "Enum : undefined element in values";
1013              
1014 4         21 return $self;
1015             }
1016              
1017              
1018             sub _inspect {
1019 6     6   12 my ($self, $data) = @_;
1020              
1021             return $self->msg(NOT_IN_LIST => $data)
1022 6 100       9 if not grep {$_ eq $data} @{$self->{-values}};
  22         52  
  6         13  
1023              
1024 4         14 return; # otherwise OK, no error
1025             }
1026              
1027              
1028             #======================================================================
1029             package Data::Domain::List;
1030             #======================================================================
1031 4     4   99 use strict;
  4         9  
  4         141  
1032 4     4   34 use warnings;
  4         8  
  4         174  
1033 4     4   23 use Carp;
  4         8  
  4         237  
1034 4     4   29 use List::MoreUtils qw/all/;
  4         6  
  4         29  
1035 4     4   4472 use Scalar::Does qw/does/;
  4         11  
  4         24  
1036             our @ISA = 'Data::Domain';
1037              
1038             sub new {
1039 13     13   21 my $class = shift;
1040 13         33 my @options = qw/-items -size -min_size -max_size -any -all/;
1041 13         33 my $self = Data::Domain::_parse_args(\@_, \@options, -items => 'arrayref');
1042 13         41 bless $self, $class;
1043              
1044 13         46 $self->_expand_range(qw/-size -min_size -max_size/);
1045 13         43 $self->_check_min_max(qw/-min_size -max_size <=/);
1046              
1047 12 100       38 if ($self->{-items}) {
1048 5 50       34 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         128 for my $bound (qw/-min_size -max_size/) {
1053             croak "$bound does not match -items"
1054 10 50 33     29 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       192 if (my $dom = $self->{$arg}) {
1061 8 100       122 $dom = [$dom] unless does($dom, 'ARRAY');
1062 8 100   9   559 all {does($_, 'Data::Domain') || does($_, 'CODE')} @$dom
  9 50       45  
1063             or croak "invalid arg to $arg in Data::Domain::List";
1064             }
1065             }
1066              
1067 12         152 return $self;
1068             }
1069              
1070              
1071             sub _inspect {
1072 38     38   72 my ($self, $data, $context) = @_;
1073 4     4   3030 no warnings 'recursion';
  4         8  
  4         2285  
1074              
1075 38 100       106 does($data, 'ARRAY')
1076             or return $self->msg(NOT_A_LIST => $data);
1077              
1078 37 100 100     800 if (defined $self->{-min_size} && @$data < $self->{-min_size}) {
1079 1         14 return $self->msg(TOO_SHORT => $self->{-min_size});
1080             }
1081              
1082 36 100 100     116 if (defined $self->{-max_size} && @$data > $self->{-max_size}) {
1083 1         8 return $self->msg(TOO_LONG => $self->{-max_size});
1084             }
1085              
1086 35 100 100     129 return unless $self->{-items} || $self->{-all} || $self->{-any};
      100        
1087              
1088             # prepare context for calling lazy subdomains
1089 33   100     342 $context ||= {root => $data,
1090             flat => {},
1091             path => []};
1092 33         74 local $context->{list} = $data;
1093              
1094             # initializing some variables
1095 33         52 my @msgs;
1096             my $has_invalid;
1097 33   100     101 my $items = $self->{-items} || [];
1098 33         47 my $n_items = @$items;
1099 33         48 my $n_data = @$data;
1100              
1101             # check the -items conditions
1102 33         81 for (my $i = 0; $i < $n_items; $i++) {
1103 50         70 local $context->{path} = [@{$context->{path}}, $i];
  50         127  
1104 50 50       119 my $subdomain = $self->_build_subdomain($items->[$i], $context)
1105             or next;
1106 50         844 $msgs[$i] = $subdomain->inspect($data->[$i], $context);
1107 50   100     244 $has_invalid ||= $msgs[$i];
1108             }
1109              
1110             # check the -all condition (can be a single domain or an arrayref of domains)
1111 33 100       92 if (my $all = $self->{-all}) {
1112 8 50       119 $all = [$all] unless does($all, 'ARRAY');
1113 8         506 my $n_all = @$all;
1114 8         33 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         40 local $context->{path} = [@{$context->{path}}, $i];
  28         77  
1118 28         62 my $subdomain = $self->_build_subdomain($all->[$j], $context);
1119 28         83 $msgs[$i] = $subdomain->inspect($data->[$i], $context);
1120 28   100     157 $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       61 if (my $any = $self->{-any}) {
1129 13 100       136 $any = [$any] unless does($any, 'ARRAY');
1130              
1131             # there must be data to inspect
1132             $n_data > $n_items
1133 13 100 33     722 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         25 foreach my $condition (@$any) {
1138 15         18 my $subdomain;
1139 15         41 for (my $i = $n_items; $i < $n_data; $i++) {
1140 31         36 local $context->{path} = [@{$context->{path}}, $i];
  31         77  
1141 31         62 $subdomain = $self->_build_subdomain($condition, $context);
1142 31         116 my $error = $subdomain->inspect($data->[$i], $context);
1143 31 100       122 next CONDITION if not $error;
1144             }
1145 4   33     33 return $self->msg(ANY => ($subdomain->{-name} || $subdomain->subclass));
1146             }
1147             }
1148              
1149 19         138 return; # OK, no error
1150             }
1151              
1152              
1153             #======================================================================
1154             package Data::Domain::Struct;
1155             #======================================================================
1156 4     4   31 use strict;
  4         9  
  4         125  
1157 4     4   25 use warnings;
  4         14  
  4         161  
1158 4     4   24 use Carp;
  4         9  
  4         249  
1159 4     4   26 use Scalar::Does qw/does/;
  4         13  
  4         25  
1160             our @ISA = 'Data::Domain';
1161              
1162             sub new {
1163 14     14   27 my $class = shift;
1164 14         33 my @options = qw/-fields -exclude -keys -values/;
1165 14         35 my $self = Data::Domain::_parse_args(\@_, \@options, -fields => 'arrayref');
1166 14         30 bless $self, $class;
1167              
1168 14   100     120 my $fields = $self->{-fields} || [];
1169 14 100       40 if (does($fields, 'ARRAY')) {
    50          
1170             # transform arrayref into hashref plus an ordered list of keys
1171 13         862 $self->{-fields_list} = [];
1172 13         31 $self->{-fields} = {};
1173 13         39 for (my $i = 0; $i < @$fields; $i += 2) {
1174 22         84 my ($key, $val) = ($fields->[$i], $fields->[$i+1]);
1175 22         33 push @{$self->{-fields_list}}, $key;
  22         42  
1176 22         79 $self->{-fields}{$key} = $val;
1177             }
1178             }
1179             elsif (does($fields, 'HASH')) {
1180             # keep given hashref, add list of keys
1181 1         47 $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       42 if (my $exclude = $self->{-exclude}) {
1189 3 50 100     9 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         195 for my $arg (qw/-keys -values/) {
1196 28 100       89 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         102 return $self;
1203             }
1204              
1205              
1206             sub _inspect {
1207 138     138   343 my ($self, $data, $context) = @_;
1208 4     4   3011 no warnings 'recursion';
  4         9  
  4         2029  
1209              
1210             # check that $data is a hashref
1211 138 100       360 does($data, 'HASH')
1212             or return $self->msg(NOT_A_HASH => $data);
1213              
1214 136         3303 my %msgs;
1215              
1216             # check if there are any forbidden fields
1217 136 100       299 if (my $exclude = $self->{-exclude}) {
1218 10         32 my @other_fields = grep {!$self->{-fields}{$_}} keys %$data;
  20         189  
1219             my @wrong_fields = match::simple::match($exclude, ['*', 'all'])
1220             ? @other_fields
1221 10 100       135 : grep {match::simple::match($_, $exclude)} @other_fields;
  5         36  
1222 10 100       74 $msgs{-exclude} = $self->msg(FORBIDDEN_FIELD => join ", ", map {"'$_'"} sort @wrong_fields)
  8         33  
1223             if @wrong_fields;
1224             }
1225              
1226             # prepare context for calling lazy subdomains
1227 136   100     413 $context ||= {root => $data,
1228             flat => {},
1229             list => [],
1230             path => []};
1231 136         195 local $context->{flat} = {%{$context->{flat}}, %$data};
  136         810  
1232              
1233             # check fields of the domain
1234 136         260 foreach my $field (@{$self->{-fields_list}}) {
  136         288  
1235 361         514 local $context->{path} = [@{$context->{path}}, $field];
  361         3449  
1236 361         757 my $field_spec = $self->{-fields}{$field};
1237 361         734 my $subdomain = $self->_build_subdomain($field_spec, $context);
1238 360         1624 my $msg = $subdomain->inspect($data->{$field}, $context);
1239 261 100       1178 $msgs{$field} = $msg if $msg;
1240             }
1241              
1242             # check the List domain for keys
1243 36 100       100 if (my $keys_dom = $self->{-keys}) {
1244 3         67 local $context->{path} = [@{$context->{path}}, "-keys"];
  3         9  
1245 3         9 my $subdomain = $self->_build_subdomain($keys_dom, $context);
1246 3         12 my $msg = $subdomain->inspect([keys %$data], $context);
1247 3 100       13 $msgs{-keys} = $msg if $msg;
1248             }
1249              
1250             # check the List domain for values
1251 36 100       97 if (my $values_dom = $self->{-values}) {
1252 3         56 local $context->{path} = [@{$context->{path}}, "-values"];
  3         12  
1253 3         7 my $subdomain = $self->_build_subdomain($values_dom, $context);
1254 3         11 my $msg = $subdomain->inspect([values %$data], $context);
1255 3 100       13 $msgs{-values} = $msg if $msg;
1256             }
1257              
1258 36 100       372 return keys %msgs ? \%msgs : undef;
1259             }
1260              
1261             #======================================================================
1262             package Data::Domain::One_of;
1263             #======================================================================
1264 4     4   35 use strict;
  4         11  
  4         365  
1265 4     4   25 use warnings;
  4         7  
  4         124  
1266 4     4   90 use Carp;
  4         14  
  4         833  
1267             our @ISA = 'Data::Domain';
1268              
1269             sub new {
1270 2     2   5 my $class = shift;
1271 2         5 my @options = qw/-options/;
1272 2         8 my $self = Data::Domain::_parse_args(\@_, \@options, -options => 'arrayref');
1273 2         6 bless $self, $class;
1274              
1275 2 50       53 Scalar::Does::does($self->{-options}, 'ARRAY')
1276             or croak "One_of: invalid options";
1277              
1278 2         54 return $self;
1279             }
1280              
1281              
1282             sub _inspect {
1283 213     213   441 my ($self, $data, $context) = @_;
1284 213         304 my @msgs;
1285 4     4   29 no warnings 'recursion';
  4         16  
  4         601  
1286              
1287 213         268 for my $subdomain (@{$self->{-options}}) {
  213         453  
1288 321 100       1249 my $msg = $subdomain->inspect($data, $context)
1289             or return; # $subdomain was successful
1290 112         265 push @msgs, $msg;
1291             }
1292 4         36 return \@msgs;
1293             }
1294              
1295              
1296             #======================================================================
1297             package Data::Domain::All_of;
1298             #======================================================================
1299 4     4   28 use strict;
  4         10  
  4         142  
1300 4     4   21 use warnings;
  4         9  
  4         112  
1301 4     4   19 use Carp;
  4         7  
  4         823  
1302             our @ISA = 'Data::Domain';
1303              
1304             sub new {
1305 1     1   3 my $class = shift;
1306 1         4 my @options = qw/-options/;
1307 1         3 my $self = Data::Domain::_parse_args(\@_, \@options, -options => 'arrayref');
1308 1         4 bless $self, $class;
1309              
1310 1 50       57 Scalar::Does::does($self->{-options}, 'ARRAY')
1311             or croak "All_of: invalid options";
1312              
1313 1         34 return $self;
1314             }
1315              
1316              
1317             sub _inspect {
1318 3     3   10 my ($self, $data, $context) = @_;
1319 3         5 my @msgs;
1320 4     4   35 no warnings 'recursion';
  4         9  
  4         551  
1321              
1322 3         4 for my $subdomain (@{$self->{-options}}) {
  3         9  
1323 6         14 my $msg = $subdomain->inspect($data, $context);
1324 6 100       20 push @msgs, $msg if $msg; # subdomain failed
1325             }
1326 3 100       24 return @msgs ? \@msgs : undef;
1327             }
1328              
1329              
1330             #======================================================================
1331             1;
1332              
1333              
1334             __END__