File Coverage

blib/lib/Data/Domain.pm
Criterion Covered Total %
statement 663 702 94.4
branch 279 334 83.5
condition 108 155 69.6
subroutine 139 147 94.5
pod 5 5 100.0
total 1194 1343 88.9


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