File Coverage

blib/lib/Config/Validator.pm
Criterion Covered Total %
statement 333 417 79.8
branch 204 302 67.5
condition 46 68 67.6
subroutine 36 41 87.8
pod 16 16 100.0
total 635 844 75.2


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Config/Validator.pm #
4             # #
5             # Description: schema based configuration validation #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Config::Validator;
14 12     12   9107 use strict;
  12         19  
  12         366  
15 12     12   57 use warnings;
  12         19  
  12         936  
16             our $VERSION = "1.2";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.35 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 12     12   11826 use No::Worries::Die qw(dief);
  12         269691  
  12         92  
24 12     12   1378 use No::Worries::Export qw(export_control);
  12         28  
  12         75  
25 12     12   940 use Scalar::Util qw(blessed reftype);
  12         24  
  12         1788  
26 12     12   10731 use URI::Escape qw(uri_escape uri_unescape);
  12         20157  
  12         93101  
27              
28             #
29             # global variables
30             #
31              
32             our(
33             $_Known, # hash reference of known schemas used by _check_type()
34             $_BuiltIn, # hash reference of built-in schemas (to validate schemas)
35             %_RE, # hash of commonly used regular expressions
36             %_DurationScale, # hash of duration suffixes
37             %_SizeScale, # hash of size suffixes
38             );
39              
40             %_DurationScale = (
41             ms => 0.001,
42             s => 1,
43             m => 60,
44             h => 60 * 60,
45             d => 60 * 60 * 24,
46             );
47              
48             %_SizeScale = (
49             b => 1,
50             kb => 1024,
51             mb => 1024 * 1024,
52             gb => 1024 * 1024 * 1024,
53             tb => 1024 * 1024 * 1024 * 1024,
54             );
55              
56             #+++############################################################################
57             # #
58             # regular expressions #
59             # #
60             #---############################################################################
61              
62             sub _init_regexp () {
63 12     12   32 my($label, $byte, $hex4, $ipv4, $ipv6, @tail);
64              
65             # simple ones
66 12         45 $_RE{boolean} = q/true|false/;
67 12         31 $_RE{integer} = q/[\+\-]?\d+/;
68 12         306 $_RE{number} = q/[\+\-]?(?=\d|\.\d)\d*(?:\.\d*)?(?:[Ee][\+\-]?\d+)?/;
69 12         28 $_RE{duration} = q/(?:\d+(?:ms|s|m|h|d))+|\d+/;
70 12         26 $_RE{size} = q/\d+[bB]?|(?:\d+\.)?\d+[kKmMgGtT][bB]/;
71             # complex ones
72 12         24 $label = q/[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?/;
73 12         20 $byte = q/25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d/;
74 12         17 $hex4 = q/[0-9a-fA-F]{1,4}/;
75 12         51 $ipv4 = qq/(($byte)\\.){3}($byte)/;
76 12         158 @tail = (
77             ":",
78             "(:($hex4)?|($ipv4))",
79             ":(($ipv4)|$hex4(:$hex4)?|)",
80             "(:($ipv4)|:$hex4(:($ipv4)|(:$hex4){0,2})|:)",
81             "((:$hex4){0,2}(:($ipv4)|(:$hex4){1,2})|:)",
82             "((:$hex4){0,3}(:($ipv4)|(:$hex4){1,2})|:)",
83             "((:$hex4){0,4}(:($ipv4)|(:$hex4){1,2})|:)",
84             );
85 12         23 $ipv6 = $hex4;
86 12         45 foreach my $tail (@tail) {
87 84         249 $ipv6 = "$hex4:($ipv6|$tail)";
88             }
89 12         101 $ipv6 = qq/:(:$hex4){0,5}((:$hex4){1,2}|:$ipv4)|$ipv6/;
90 12         60 $_RE{hostname} = qq/($label\\.)*$label/;
91 12         31 $_RE{ipv4} = $ipv4;
92 12         47 $_RE{ipv6} = $ipv6;
93             # improve some of them
94 12         32 foreach my $name (qw(hostname ipv4 ipv6)) {
95 36         356 $_RE{$name} =~ s/\(/(?:/g;
96             }
97             # compile them all
98 12         61 foreach my $name (keys(%_RE)) {
99 96         7331 $_RE{$name} = qr/^(?:$_RE{$name})$/;
100             }
101             }
102              
103             _init_regexp();
104              
105             #+++############################################################################
106             # #
107             # helper functions #
108             # #
109             #---############################################################################
110              
111             #
112             # stringify any scalar, including undef
113             #
114              
115             sub _string ($) {
116 17     17   26 my($scalar) = @_;
117              
118 17 50       126 return(defined($scalar) ? "$scalar" : "");
119             }
120              
121             #
122             # format an error
123             #
124              
125             sub _errfmt (@);
126             sub _errfmt (@) {
127 570     570   860 my(@errors) = @_;
128 570         590 my($string, $tmp);
129              
130 570 100       1289 return("") unless @errors;
131 330         481 $string = shift(@errors);
132 330         578 foreach my $error (@errors) {
133 297 50       739 $tmp = ref($error) ? _errfmt(@{ $error }) : $error;
  297         827  
134 297 100       953 next unless length($tmp);
135 57         432 $tmp =~ s/^/ /mg;
136 57         241 $string .= "\n" . $tmp;
137             }
138 330         1225 return($string);
139             }
140              
141             #
142             # expand a duration string and return the corresponding number of seconds
143             #
144              
145             sub expand_duration ($) {
146 10     10 1 5924 my($value) = @_;
147 10         12 my($result);
148              
149 10 100       58 if ($value =~ /^(\d+(ms|s|m|h|d))+$/) {
150 7         9 $result = 0;
151 7         40 while ($value =~ /(\d+)(ms|s|m|h|d)/g) {
152 10         59 $result += $1 * $_DurationScale{$2};
153             }
154             } else {
155 3         4 $result = $value;
156             }
157 10         29 return($result);
158             }
159              
160             #
161             # expand a size string and return the corresponding number of bytes
162             #
163              
164             sub expand_size ($) {
165 10     10 1 3185 my($value) = @_;
166              
167 10 100       38 if ($value =~ /^(.+?)([kmgt]?b)$/i) {
168 7         35 return(int($1 * $_SizeScale{lc($2)} + 0.5));
169             } else {
170 3         5 return($value);
171             }
172             }
173              
174             #
175             # test if a boolean is true or false
176             #
177              
178             sub is_true ($) {
179 350     350 1 1734 my($value) = @_;
180              
181 350 100       640 return(undef) unless defined($value);
182 346   100     3032 return($value and not ref($value) and $value eq "true");
183             }
184              
185             sub is_false ($) {
186 5     5 1 10 my($value) = @_;
187              
188 5 100       17 return(undef) unless defined($value);
189 4   100     34 return($value and not ref($value) and $value eq "false");
190             }
191              
192             #
193             # return the given thing as a list
194             #
195              
196             sub listof ($) {
197 5     5 1 9 my($thing) = @_;
198              
199 5 100       17 return() unless defined($thing);
200 4 100       13 return(@{ $thing }) if ref($thing) eq "ARRAY";
  3         19  
201 1         6 return($thing);
202             }
203              
204             #+++############################################################################
205             # #
206             # conversion helper functions #
207             # #
208             #---############################################################################
209              
210             #
211             # string -> hash
212             #
213              
214             sub string2hash ($) {
215 3     3 1 1238 my($string) = @_;
216 3         4 my(%hash);
217              
218 3         9 foreach my $kv (split(/\s+/, $string)) {
219 3 50       42 if ($kv =~ /^([^\=]+)=(.*)$/) {
220 3         10 $hash{uri_unescape($1)} = uri_unescape($2);
221             } else {
222 0         0 dief("invalid hash key=value: %s", $kv);
223             }
224             }
225 3 50       38 return(%hash) if wantarray();
226 0         0 return(\%hash);
227             }
228              
229             #
230             # hash -> string
231             #
232              
233             sub hash2string (@) {
234 5     5 1 17 my(@args) = @_;
235 5         25 my($hash, @kvs);
236              
237 5 100 66     25 if (@args == 1 and ref($args[0]) eq "HASH") {
238 4         8 $hash = $args[0];
239             } else {
240 1         2 $hash = { @args };
241             }
242 5         6 foreach my $key (sort(keys(%{ $hash }))) {
  5         19  
243 5         39 push(@kvs, uri_escape($key) . "=" . uri_escape($hash->{$key}));
244             }
245 5         104 return(join(" ", @kvs));
246             }
247              
248             #
249             # treeify
250             #
251              
252             sub treeify ($);
253             sub treeify ($) {
254 4     4 1 17 my($hash) = @_;
255              
256 4         29 foreach my $key (grep(/-/, keys(%{ $hash }))) {
  4         20  
257 3 50       17 if ($key =~ /^(\w+)-(.+)$/) {
258 3         14 $hash->{$1}{$2} = delete($hash->{$key});
259             } else {
260 0         0 dief("unexpected configuration name: %s", $key);
261             }
262             }
263 4         8 foreach my $value (values(%{ $hash })) {
  4         10  
264 7 100       100 treeify($value) if ref($value) eq "HASH";
265             }
266             }
267              
268             #
269             # return the value of the given option in a treeified hash
270             #
271              
272             sub treeval ($$);
273             sub treeval ($$) {
274 12     12 1 1033 my($hash, $name) = @_;
275              
276 12 100       66 return($hash->{$name}) if exists($hash->{$name});
277 4 50       28 if ($name =~ /^(\w+)-(.+)$/) {
278 4 50       18 return() unless $hash->{$1};
279 4         17 return(treeval($hash->{$1}, $2));
280             }
281 0         0 return();
282             }
283              
284             #+++############################################################################
285             # #
286             # built-in schemas #
287             # #
288             #---############################################################################
289              
290             #
291             # check that a type is valid
292             #
293              
294             sub _check_type ($$$);
295             sub _check_type ($$$) {
296 58     58   99 my($valid, $schema, $data) = @_;
297              
298 58 100       334 return() if $data =~ /^[a-z46]+$/;
299 19 50       120 return() if $data =~ /^(ref|isa)\(\*\)$/;
300 19 100       65 return() if $data =~ /^(ref|isa)\([\w\:]+\)$/;
301 16 100       67 if ($data =~ /^(list\??|table)\((.+)\)$/) {
302 7         24 return(_check_type($valid, $schema, $2));
303             }
304 9 50       38 if ($data =~ /^valid\((.+)\)$/) {
305 9 100       48 return() if $_Known->{$1};
306 1         4 return("unknown schema: $1");
307             }
308 0         0 return("unexpected type: $data");
309             }
310              
311             #
312             # schema of a "type"
313             #
314              
315             $_BuiltIn->{type} = {
316             type => "string",
317             match => qr/ ^
318             ( anything # really anything
319             | undef # undef
320             | undefined # "
321             | defined # not undef
322             | string # any string
323             | boolean # either 'true' or 'false'
324             | number # any number
325             | integer # any integer
326             | duration # any duration, i.e. numbers with hms suffixes
327             | size # any size, i.e. number with optional byte-suffix
328             | hostname # host name
329             | ipv4 # IPv4 address
330             | ipv6 # IPv6 address
331             | reference # any reference, blessed or not
332             | ref\(\*\) # "
333             | blessed # any blessed reference
334             | object # "
335             | isa\(\*\) # "
336             | unblessed # any reference which is not blessed
337             | code # a code reference (aka ref(CODE))
338             | regexp # a regular expression (see is_regexp())
339             | list # an homogeneous list
340             | list\(.+\) # idem but with the given subtype
341             | list\?\(.+\) # shortcut: list?(X) means either X or list(X)
342             | table # an homogeneous table
343             | table\(.+\) # idem but with the given subtype
344             | struct # a structure, i.e. a table with known keys
345             | ref\(.+\) # a reference of the given kind
346             | isa\(.+\) # an object of the given kind
347             | valid\(.+\) # something valid according to the named schema
348             ) $ /x,
349             check => \&_check_type,
350             };
351              
352             #
353             # check that a schema is valid
354             #
355              
356             sub _check_schema ($$$);
357             sub _check_schema ($$$) {
358 50     50   125 my($valid, $schema, $data) = @_;
359 50         74 my($field);
360              
361 50         70 $field = "min";
362 50 100 100     222 goto unexpected if defined($data->{$field})
363             and not $data->{type} =~ /^(string|number|integer|list.*|table.*)$/;
364 49         82 $field = "max";
365 49 50 66     375 goto unexpected if defined($data->{$field})
366             and not $data->{type} =~ /^(string|number|integer|list.*|table.*)$/;
367 49         77 $field = "match";
368 49 50 66     180 goto unexpected if defined($data->{$field})
369             and not $data->{type} =~ /^(string|table.*)$/;
370 49         78 $field = "subtype";
371 49 100       215 if ($data->{type} =~ /^(list|table)$/) {
372 1 50       3 goto missing unless defined($data->{$field});
373             } else {
374 48 50       158 goto unexpected if defined($data->{$field});
375             }
376 49         68 $field = "fields";
377 49 100       135 if ($data->{type} =~ /^(struct)$/) {
378 3 50       15 goto missing unless defined($data->{$field});
379             } else {
380 46 50       137 goto unexpected if defined($data->{$field});
381             }
382 49         99 return();
383 1         7 unexpected:
384             return(sprintf("unexpected schema field for type %s: %s",
385             $data->{type}, $field));
386 0         0 missing:
387             return(sprintf("missing schema field for type %s: %s",
388             $data->{type}, $field));
389             }
390              
391             #
392             # schema of a "schema"
393             #
394              
395             $_BuiltIn->{schema} = {
396             type => "struct",
397             fields => {
398             type => { type => "list?(valid(type))" },
399             subtype => { type => "valid(schema)", optional => "true" },
400             fields => { type => "table(valid(schema))", optional => "true" },
401             optional => { type => "boolean", optional => "true" },
402             min => { type => "number", optional => "true" },
403             max => { type => "number", optional => "true" },
404             match => { type => "regexp", optional => "true" },
405             check => { type => "code", optional => "true" },
406             },
407             check => \&_check_schema,
408             };
409              
410             #+++############################################################################
411             # #
412             # options helpers #
413             # #
414             #---############################################################################
415              
416             #
417             # schema -> options
418             #
419              
420             sub _options ($$$@);
421             sub _options ($$$@) {
422 0     0   0 my($valid, $schema, $type, @path) = @_;
423 0         0 my(@list);
424              
425 0   0     0 $type ||= $schema->{type};
426             # terminal
427 0 0       0 return(join("-", @path) . "=s") if $type eq "string";
428 0 0       0 return(join("-", @path) . "=f") if $type eq "number";
429 0 0       0 return(join("-", @path) . "=i") if $type eq "integer";
430 0 0       0 return(join("-", @path) . "!") if $type eq "boolean";
431             # assumed to come from strings
432 0 0 0     0 return(join("-", @path) . "=s")
433             if $type =~ /^isa\(.+\)$/ or $type eq "table(string)";
434             # recursion
435 0 0       0 if ($type =~ /^list\?\((.+)\)$/) {
436 0         0 return(map($_ . "\@", _options($valid, $schema, $1, @path)));
437             }
438 0 0       0 if ($type =~ /^valid\((.+)\)$/) {
439 0 0       0 dief("options(): unknown schema: %s", $1) unless $valid->{$1};
440 0         0 return(_options($valid, $valid->{$1}, undef, @path));
441             }
442 0 0       0 if ($type eq "struct") {
443 0         0 foreach my $field (keys(%{ $schema->{fields} })) {
  0         0  
444 0         0 push(@list, _options($valid, $schema->{fields}{$field},
445             undef, @path, $field));
446             }
447 0         0 return(@list);
448             }
449             # unsupported
450 0         0 dief("options(): unsupported type: %s", $type);
451             }
452              
453             #
454             # treat the given options as mutually exclusive
455             #
456              
457             sub mutex ($@) {
458 0     0 1 0 my($hash, @options) = @_;
459 0         0 my(@list);
460              
461 0         0 foreach my $opt (@options) {
462 0 0       0 next unless defined(treeval($hash, $opt));
463 0         0 push(@list, $opt);
464 0 0       0 dief("options %s and %s are mutually exclusive", @list) if @list == 2;
465             }
466             }
467              
468             #
469             # if the first option is set, all the others are required
470             #
471              
472             sub reqall ($$@) {
473 0     0 1 0 my($hash, $opt1, @options) = @_;
474              
475 0 0 0     0 return unless not defined($opt1) or defined(treeval($hash, $opt1));
476 0         0 foreach my $opt2 (@options) {
477 0 0       0 next if defined(treeval($hash, $opt2));
478 0 0       0 dief("option %s requires option %s", $opt1, $opt2) if defined($opt1);
479 0         0 dief("option %s is required", $opt2);
480             }
481             }
482              
483             #
484             # if the first option is set, one at least of the others is required
485             #
486              
487             sub reqany ($$@) {
488 0     0 1 0 my($hash, $opt1, @options) = @_;
489 0         0 my($req);
490              
491 0 0 0     0 return unless not defined($opt1) or defined(treeval($hash, $opt1));
492 0         0 foreach my $opt2 (@options) {
493 0 0       0 return if defined(treeval($hash, $opt2));
494             }
495 0 0       0 if (@options <= 2) {
496 0         0 $req = join(" or ", @options);
497             } else {
498 0         0 push(@options, join(" or ", splice(@options, -2)));
499 0         0 $req = join(", ", @options);
500             }
501 0 0       0 dief("option %s requires option %s", $opt1, $req) if defined($opt1);
502 0         0 dief("option %s is required", $req);
503             }
504              
505             #+++############################################################################
506             # #
507             # traverse helpers #
508             # #
509             #---############################################################################
510              
511             #
512             # traverse data
513             #
514              
515             sub _traverse_list ($$$$$$@) {
516 1     1   6 my($callback, $valid, $schema, $reftype, $subtype, $data, @path) = @_;
517              
518 1 50       6 return unless $reftype eq "ARRAY";
519 1         2 foreach my $val (@{ $data }) {
  1         3  
520 1         8 _traverse($callback, $valid, $schema, $subtype,
521             $val, @path, 0);
522             }
523             }
524              
525             sub _traverse_table ($$$$$$@) {
526 1     1   5 my($callback, $valid, $schema, $reftype, $subtype, $data, @path) = @_;
527              
528 1 50       6 return unless $reftype eq "HASH";
529 1         2 foreach my $key (keys(%{ $data })) {
  1         6  
530 1         3 _traverse($callback, $valid, $schema, $subtype,
531             $data->{$key}, @path, $key);
532             }
533             }
534              
535             sub _traverse_struct ($$$$$$@) {
536 2     2   6 my($callback, $valid, $schema, $reftype, $subtype, $data, @path) = @_;
537              
538 2 50       8 return unless $reftype eq "HASH";
539 2         4 foreach my $key (keys(%{ $schema->{fields} })) {
  2         9  
540 6 50       25 next unless exists($data->{$key});
541 6         31 _traverse($callback, $valid, $schema->{fields}{$key}, undef,
542             $data->{$key}, @path, $key);
543             }
544             }
545              
546             sub _traverse ($$$$$@);
547             sub _traverse ($$$$$@) {
548 18     18   57 my($callback, $valid, $schema, $type, $data, @path) = @_;
549 18         22 my($reftype, $subtype);
550              
551             # set the type if missing
552 18   66     87 $type ||= $schema->{type};
553             # call the callback and stop unless we are told to continue
554 18 50       56 return unless $callback->($valid, $schema, $type, $_[4], @path);
555             # terminal
556 18 100       28375 return if $type =~ /^(boolean|number|integer)$/;
557 12 50       47 return if $type =~ /^(duration|size|hostname|ipv[46])$/;
558 12 50       65 return if $type =~ /^(undef|undefined|defined|blessed|unblessed)$/;
559 12 50       36 return if $type =~ /^(anything|string|regexp|object|reference|code)$/;
560             # recursion
561 12   100     72 $reftype = reftype($data) || "";
562 12 100       99 if ($type =~ /^valid\((.+)\)$/) {
563 8 50       39 dief("traverse(): unknown schema: %s", $1) unless $valid->{$1};
564 8         32 _traverse($callback, $valid, $valid->{$1}, undef, $_[4], @path);
565 8         36 return;
566             }
567 4 100       17 if ($type eq "struct") {
568 2         9 _traverse_struct($callback, $valid, $schema,
569             $reftype, $subtype, $data, @path);
570 2         9 return;
571             }
572 2 50       8 if ($type =~ /^list$/) {
573 0         0 _traverse_list($callback, $valid, $schema->{subtype},
574             $reftype, $subtype, $data, @path);
575 0         0 return;
576             }
577 2 100       13 if ($type =~ /^list\((.+)\)$/) {
578 1         5 _traverse_list($callback, $valid, $schema,
579             $reftype, $1, $data, @path);
580 1         5 return;
581             }
582 1 50       5 if ($type =~ /^list\?\((.+)\)$/) {
583 0 0       0 if ($reftype eq "ARRAY") {
584 0         0 _traverse_list($callback, $valid, $schema,
585             $reftype, $1, $data, @path);
586             } else {
587 0         0 _traverse($callback, $valid, $schema,
588             $1, $_[4], @path);
589             }
590 0         0 return;
591             }
592 1 50       5 if ($type =~ /^table$/) {
593 0         0 _traverse_table($callback, $valid, $schema->{subtype},
594             $reftype, $subtype, $data, @path);
595 0         0 return;
596             }
597 1 50       9 if ($type =~ /^table\((.+)\)$/) {
598 1         5 _traverse_table($callback, $valid, $schema,
599             $reftype, $1, $data, @path);
600 1         7 return;
601             }
602             # unsupported
603 0         0 dief("traverse(): unsupported type: %s", $type);
604             }
605              
606             #+++############################################################################
607             # #
608             # validation helpers #
609             # #
610             #---############################################################################
611              
612             #
613             # test if something is a regular expression
614             #
615              
616             if ($] >= 5.010) {
617             require re;
618             re->import(qw(is_regexp));
619             } else {
620             *is_regexp = sub { return(ref($_[0]) eq "Regexp") };
621             }
622              
623             #
624             # validate that a value is within a numerical range
625             #
626              
627             sub _validate_range ($$$$) {
628 44     44   94 my($what, $value, $min, $max) = @_;
629              
630 44 100 100     214 return(sprintf("%s is not >= %s: %s", $what, $min, $value))
631             if defined($min) and not $value >= $min;
632 37 100 66     172 return(sprintf("%s is not <= %s: %s", $what, $max, $value))
633             if defined($max) and not $value <= $max;
634 30         63 return();
635             }
636              
637             #
638             # validate a list of homogeneous elements
639             #
640              
641             sub _validate_list ($$$) {
642 13     13   16 my($valid, $schema, $data) = @_;
643 13         18 my(@errors, $index, $element);
644              
645 13 100 66     49 @errors = _validate_range("size", scalar(@{ $data }),
  10         29  
646             $schema->{min}, $schema->{max})
647             if defined($schema->{min}) or defined($schema->{max});
648 13 100       37 return(@errors) if @errors;
649 9         10 $index = 0;
650 9         12 foreach my $tmp (@{ $data }) {
  9         17  
651 12         15 $element = $tmp; # preserved outside loop
652 12         25 @errors = _validate($valid, $schema->{subtype}, $element);
653 12 100       38 goto invalid if @errors;
654 9         21 $index++;
655             }
656 6         13 return();
657 3         8 invalid:
658             return(sprintf("invalid element %d: %s",
659             $index, _string($element)), \@errors);
660             }
661              
662             #
663             # validate a table of homogeneous elements
664             #
665              
666             sub _validate_table ($$$) {
667 44     44   79 my($valid, $schema, $data) = @_;
668 44         55 my(@errors, $key);
669              
670 44 100 66     334 @errors = _validate_range("size", scalar(keys(%{ $data })),
  6         30  
671             $schema->{min}, $schema->{max})
672             if defined($schema->{min}) or defined($schema->{max});
673 44 100       126 return(@errors) if @errors;
674 43         62 foreach my $tmp (keys(%{ $data })) {
  43         130  
675 57         107 $key = $tmp; # preserved outside loop
676 57 100 100     221 @errors = (sprintf("key does not match %s: %s",
677             $schema->{match}, $key))
678             if defined($schema->{match}) and not $key =~ $schema->{match};
679 57 100       167 goto invalid if @errors;
680 56         235 @errors = _validate($valid, $schema->{subtype}, $data->{$key});
681 56 100       262 goto invalid if @errors;
682             }
683 37         104 return();
684 6         23 invalid:
685             return(sprintf("invalid element %s: %s",
686             $key, _string($data->{$key})), \@errors);
687             }
688              
689             #
690             # validate a struct, i.e. a hash with known fields
691             #
692              
693             sub _validate_struct ($$$) {
694 59     59   87 my($valid, $schema, $data) = @_;
695 59         76 my(@errors, $key);
696              
697             # check the missing fields
698 59         76 foreach my $tmp (keys(%{ $schema->{fields} })) {
  59         281  
699 434         514 $key = $tmp; # preserved outside loop
700 434 100       896 next if exists($data->{$key});
701 345 100       861 next if is_true($schema->{fields}{$key}{optional});
702 3         24 return(sprintf("missing field: %s", $key));
703             }
704             # check the existing fields
705 56         109 foreach my $tmp (keys(%{ $data })) {
  56         181  
706 88         130 $key = $tmp; # preserved outside loop
707 88 100       252 return(sprintf("unexpected field: %s", $key))
708             unless $schema->{fields}{$key};
709 87         327 @errors = _validate($valid, $schema->{fields}{$key}, $data->{$key});
710 87 100       292 goto invalid if @errors;
711             }
712 52         144 return();
713 3         11 invalid:
714             return(sprintf("invalid field %s: %s",
715             $key, _string($data->{$key})), \@errors);
716             }
717              
718             #
719             # validate something using multiple possible types
720             #
721              
722             sub _validate_multiple ($$$@) {
723 58     58   1075 my($valid, $schema, $data, @types) = @_;
724 58         83 my(@errors, %tmpschema, @tmperrors);
725              
726 58         75 %tmpschema = %{ $schema };
  58         201  
727 58         123 foreach my $type (@types) {
728 65         124 $tmpschema{type} = $type;
729 65         186 @tmperrors = _validate($valid, \%tmpschema, $data);
730 65 100       291 return() unless @tmperrors;
731 12         35 push(@errors, [ @tmperrors ]);
732             }
733 5         15 return(sprintf("invalid data (none of the types could be validated): %s",
734             _string($data)), @errors);
735             }
736              
737             #
738             # validate data (non-reference types)
739             #
740              
741             sub _validate_data_nonref ($$) {
742 418     418   627 my($schema, $data) = @_;
743 418         427 my($type, @errors);
744              
745 418         605 $type = $schema->{type};
746 418 100       1550 if ($type eq "string") {
    100          
    50          
747 67 100 66     382 @errors = _validate_range
748             ("length", length($data), $schema->{min}, $schema->{max})
749             if defined($schema->{min}) or defined($schema->{max});
750 67 100 100     844 @errors = (sprintf("value does not match %s: %s",
      100        
751             $schema->{match}, $data))
752             if not @errors and defined($schema->{match})
753             and not $data =~ $schema->{match};
754             } elsif ($type =~ /^(boolean|hostname|ipv[46])$/) {
755 257 100       3698 goto invalid unless $data =~ $_RE{$type};
756             # additional hard-coded checks for host names...
757 136 100       288 if ($type eq "hostname") {
758 13 100       81 goto invalid if ".$data." =~ /\.\d+\./;
759 10         22 @errors = _validate_range("length", length($data), 1, 255);
760             }
761             } elsif ($type =~ /^(integer|number|duration|size)$/) {
762 94 100       1092 goto invalid unless $data =~ $_RE{$type};
763 68 100 66     349 @errors = _validate_range
764             ("value", $data, $schema->{min}, $schema->{max})
765             if defined($schema->{min}) or defined($schema->{max});
766             } else {
767 0         0 return(sprintf("unexpected type: %s", $type));
768             }
769 268 100       927 return() unless @errors;
770 162         898 invalid:
771             return(sprintf("invalid %s: %s", $type, $data), \@errors);
772             }
773              
774             #
775             # validate data (reference types)
776             #
777              
778             ## no critic (ProhibitCascadingIfElse, ProhibitExcessComplexity)
779             sub _validate_data_ref ($$$$) {
780 164     164   306 my($valid, $schema, $data, $reftype) = @_;
781 164         186 my(@errors, %tmpschema, $blessed);
782              
783 164         400 $blessed = defined(blessed($data));
784 164 100       1793 if ($schema->{type} =~ /^(blessed|object|isa\(\*\))$/) {
    50          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    50          
785 7 100       90 goto invalid unless $blessed;
786             } elsif ($schema->{type} eq "unblessed") {
787 0 0       0 goto invalid if $blessed;
788             } elsif ($schema->{type} eq "code") {
789 9 100       163 goto invalid unless $reftype eq "CODE";
790             } elsif ($schema->{type} eq "regexp") {
791 10 100       136 goto invalid unless is_regexp($data);
792             } elsif ($schema->{type} eq "list") {
793 3 50       7 goto invalid unless $reftype eq "ARRAY";
794 3         8 @errors = _validate_list($valid, $schema, $data);
795             } elsif ($schema->{type} =~ /^list\((.+)\)$/) {
796 10 50       23 goto invalid unless $reftype eq "ARRAY";
797 10         13 %tmpschema = %{ $schema };
  10         42  
798 10         52 $tmpschema{subtype} = { type => $1 };
799 10         24 @errors = _validate_list($valid, \%tmpschema, $data);
800             } elsif ($schema->{type} eq "table") {
801 0 0       0 goto invalid unless $reftype eq "HASH";
802 0         0 @errors = _validate_table($valid, $schema, $data);
803             } elsif ($schema->{type} =~ /^table\((.+)\)$/) {
804 45 100       143 goto invalid unless $reftype eq "HASH";
805 44         58 %tmpschema = %{ $schema };
  44         185  
806 44         229 $tmpschema{subtype} = { type => $1 };
807 44         159 @errors = _validate_table($valid, \%tmpschema, $data);
808             } elsif ($schema->{type} eq "struct") {
809 59 50       176 goto invalid unless $reftype eq "HASH";
810 59         165 @errors = _validate_struct($valid, $schema, $data);
811             } elsif ($schema->{type} =~ /^ref\((.+)\)$/) {
812 14 100       171 goto invalid unless $reftype eq $1;
813             } elsif ($schema->{type} =~ /^isa\((.+)\)$/) {
814 7 100 66     209 goto invalid unless $blessed and $data->isa($1);
815             } else {
816 0         0 return(sprintf("unexpected type: %s", $schema->{type}));
817             }
818 133 100       465 return() unless @errors;
819 52         348 invalid:
820             return(sprintf("invalid %s: %s", $schema->{type}, $data), \@errors);
821             }
822             ## use critic
823              
824             #
825             # validate something
826             #
827              
828             sub _validate ($$$);
829             sub _validate ($$$) {
830 849     849   1271 my($valid, $schema, $data) = @_;
831 849         932 my($type, @errors, $reftype, $blessed, %tmpschema);
832              
833 849         1495 $type = $schema->{type};
834             # check multiple types
835 849 50       1973 if (ref($type) eq "ARRAY") {
836 0         0 return(_validate_multiple($valid, $schema, $data, @{ $type }));
  0         0  
837             }
838             # check list?(X)
839 849 100       1881 if ($type =~ /^list\?\((.+)\)$/) {
840 58         307 return(_validate_multiple($valid, $schema, $data, $1, "list($1)"));
841             }
842             # check valid(X)
843 791 100       1676 if ($type =~ /^valid\((.+)\)$/) {
844 105 50       397 return(sprintf("unexpected schema: %s", $1)) unless $valid->{$1};
845 105         380 return(_validate($valid, $valid->{$1}, $data));
846             }
847             # check anything
848 686 100       1308 goto good if $type eq "anything";
849             # check if defined
850 675 100       1315 if ($type =~ /^(undef|undefined)$/) {
851 11 100       42 goto invalid if defined($data);
852 1         5 goto good;
853             }
854 664 100       1489 return(sprintf("invalid %s: ", $type))
855             unless defined($data);
856 650 100       1230 goto good if $type eq "defined";
857 640         1537 $reftype = reftype($data);
858 640 100 100     3853 if ($type =~ /^(string|boolean|number|integer)$/ or
859             $type =~ /^(duration|size|hostname|ipv[46])$/) {
860             # check reference type (for non-reference)
861 436 100       927 goto invalid if defined($reftype);
862 418         845 @errors = _validate_data_nonref($schema, $data);
863             } else {
864             # check reference type (for reference)
865 204 100       583 goto invalid unless defined($reftype);
866 171 100       453 goto good if $type =~ /^(reference|ref\(\*\))$/;
867 164         427 @errors = _validate_data_ref($valid, $schema, $data, $reftype);
868             }
869 582 100       1795 return(@errors) if @errors;
870 397 100       993 good:
871             @errors = $schema->{check}->($valid, $schema, $data) if $schema->{check};
872 397 100       1714 return() unless @errors;
873 63         405 invalid:
874             return(sprintf("invalid %s: %s", $type, $data), \@errors);
875             }
876              
877             #+++############################################################################
878             # #
879             # object oriented interface #
880             # #
881             #---############################################################################
882              
883             #
884             # create a validator object
885             #
886              
887             sub new : method {
888 36     36 1 11853 my($class, $self, @errors);
889              
890 36         256 $class = shift(@_);
891 36         79 $self = {};
892             # find out which schema(s) to use
893 36 100       175 if (@_ == 0) {
    100          
    100          
894 1         4 $self->{schema} = $_BuiltIn;
895             } elsif (@_ == 1) {
896 33         123 $self->{schema}{""} = $_[0];
897             } elsif (@_ % 2 == 0) {
898 1         8 $self->{schema} = { @_ };
899             } else {
900 1         9 dief("new(): unexpected number of arguments: %d", scalar(@_));
901             }
902             # validate them
903             {
904 35         51 local $_Known = $self->{schema};
  35         97  
905 35         229 @errors = _validate($_BuiltIn, { type => "table(valid(schema))" },
906             $self->{schema});
907             }
908 35 100       129 dief("new(): invalid schema: %s", _errfmt(@errors)) if @errors;
909             # so far so good!
910 31         107 bless($self, $class);
911 31         91 return($self);
912             }
913              
914             #
915             # convert to a list of options
916             #
917              
918             sub options : method {
919 0     0 1 0 my($self, $schema);
920              
921 0         0 $self = shift(@_);
922             # find out which schema to convert to options
923 0 0       0 if (@_ == 0) {
    0          
924 0 0       0 dief("options(): no default schema")
925             unless $self->{schema}{""};
926 0         0 $schema = $self->{schema}{""};
927             } elsif (@_ == 1) {
928 0         0 $schema = shift(@_);
929 0 0       0 dief("options(): unknown schema: %s", $schema)
930             unless $self->{schema}{$schema};
931 0         0 $schema = $self->{schema}{$schema};
932             } else {
933 0         0 dief("options(): unexpected number of arguments: %d", scalar(@_));
934             }
935             # convert to options
936 0         0 return(_options($self->{schema}, $schema, undef));
937             }
938              
939             #
940             # validate the given data
941             #
942              
943             sub validate : method {
944 489     489 1 236113 my($self, $data, $schema, @errors);
945              
946 489         785 $self = shift(@_);
947             # find out what to validate against
948 489 50       1010 if (@_ == 1) {
    0          
949 489         616 $data = shift(@_);
950 489 50       1405 dief("validate(): no default schema")
951             unless $self->{schema}{""};
952 489         779 $schema = $self->{schema}{""};
953             } elsif (@_ == 2) {
954 0         0 $data = shift(@_);
955 0         0 $schema = shift(@_);
956 0 0       0 dief("validate(): unknown schema: %s", $schema)
957             unless $self->{schema}{$schema};
958 0         0 $schema = $self->{schema}{$schema};
959             } else {
960 0         0 dief("validate(): unexpected number of arguments: %d", scalar(@_));
961             }
962             # validate data
963             {
964 489         484 local $_Known = $self->{schema};
  489         807  
965 489         1047 @errors = _validate($self->{schema}, $schema, $data);
966             }
967 489 100       1690 dief("validate(): %s", _errfmt(@errors)) if @errors;
968             }
969              
970             #
971             # traverse the given data
972             #
973              
974             sub traverse : method {
975 2     2 1 512 my($self, $callback, $data, $schema);
976              
977 2         6 $self = shift(@_);
978             # find out what to traverse
979 2 50       21 if (@_ == 2) {
    50          
980 0         0 $callback = shift(@_);
981 0         0 $data = shift(@_);
982 0 0       0 dief("traverse(): no default schema")
983             unless $self->{schema}{""};
984 0         0 $schema = $self->{schema}{""};
985             } elsif (@_ == 3) {
986 2         4 $callback = shift(@_);
987 2         4 $data = shift(@_);
988 2         4 $schema = shift(@_);
989 2 50       20 dief("traverse(): unknown schema: %s", $schema)
990             unless $self->{schema}{$schema};
991 2         5 $schema = $self->{schema}{$schema};
992             } else {
993 0         0 dief("traverse(): unexpected number of arguments: %d", scalar(@_));
994             }
995             # traverse data
996 2         10 _traverse($callback, $self->{schema}, $schema, undef, $data);
997             }
998              
999             #
1000             # export control
1001             #
1002              
1003             sub import : method {
1004 11     11   273 my($pkg, %exported);
1005              
1006 11         28 $pkg = shift(@_);
1007 11         24 foreach my $name (qw(string2hash hash2string treeify treeval
1008             expand_duration expand_size
1009             is_true is_false is_regexp listof
1010             mutex reqall reqany)) {
1011 143         241 $exported{$name}++;
1012             }
1013 11         78 export_control(scalar(caller()), $pkg, \%exported, @_);
1014             }
1015              
1016             1;
1017              
1018             __DATA__