File Coverage

blib/lib/Data/Domain.pm
Criterion Covered Total %
statement 751 800 93.8
branch 318 384 82.8
condition 110 157 70.0
subroutine 162 172 94.1
pod 11 11 100.0
total 1352 1524 88.7


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