File Coverage

lib/XML/Compile/Schema/BuiltInFacets.pm
Criterion Covered Total %
statement 145 195 74.3
branch 57 104 54.8
condition 10 36 27.7
subroutine 49 66 74.2
pod 1 1 100.0
total 262 402 65.1


line stmt bran cond sub pod time code
1             # Copyrights 2006-2019 by [Mark Overmeer ].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.02.
5             # This code is part of distribution XML-Compile. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package XML::Compile::Schema::BuiltInFacets;
10 50     50   325 use vars '$VERSION';
  50         99  
  50         2502  
11             $VERSION = '1.62';
12              
13 50     50   289 use base 'Exporter';
  50         99  
  50         6604  
14              
15 50     50   310 use warnings;
  50         92  
  50         1083  
16 50     50   269 use strict;
  50         114  
  50         1631  
17 50     50   295 no warnings 'recursion';
  50         121  
  50         3531  
18              
19             our @EXPORT = qw/builtin_facet/;
20              
21 50     50   379 use Log::Report 'xml-compile';
  50         101  
  50         312  
22              
23 50     50   11959 use Math::BigInt;
  50         98  
  50         420  
24 50     50   17378 use Math::BigFloat;
  50         107  
  50         240  
25 50     50   28135 use XML::LibXML; # for ::RegExp
  50         103  
  50         460  
26 50     50   8401 use XML::Compile::Util qw/SCHEMA2001 pack_type duration2secs/;
  50         104  
  50         2506  
27              
28 50     50   324 use POSIX qw/DBL_MAX_10_EXP DBL_DIG/;
  50         108  
  50         385  
29              
30             # depend on Perl's compile flags
31 50     50   5656 use constant INT_MAX => int((sprintf"%u\n",-1)/2);
  50         119  
  50         4623  
32 50     50   302 use constant INT_MIN => -1 - INT_MAX;
  50         102  
  50         132754  
33              
34              
35             my %facets_simple =
36             ( enumeration => \&_enumeration
37             , fractionDigits => \&_s_fractionDigits
38             , length => \&_s_length
39             , maxExclusive => \&_s_maxExclusive
40             , maxInclusive => \&_s_maxInclusive
41             , maxLength => \&_s_maxLength
42             , maxScale => undef # ignore
43             , minExclusive => \&_s_minExclusive
44             , minInclusive => \&_s_minInclusive
45             , minLength => \&_s_minLength
46             , minScale => undef # ignore
47             , pattern => \&_pattern
48             , totalDigits => \&_s_totalDigits
49             , whiteSpace => \&_s_whiteSpace
50             , _totalFracDigits=> \&_s_totalFracDigits
51             );
52              
53             my %facets_list =
54             ( enumeration => \&_enumeration
55             , length => \&_list_length
56             , maxLength => \&_list_maxLength
57             , minLength => \&_list_minLength
58             , pattern => \&_pattern
59             , whiteSpace => \&_list_whiteSpace
60             );
61              
62             my %facets_date = # inclusive or exclusive times is rather useless.
63             ( enumeration => \&_enumeration
64             , explicitTimeZone=> \&_date_expl_tz
65             , maxExclusive => \&_date_max
66             , maxInclusive => \&_date_max
67             , minExclusive => \&_date_min
68             , minInclusive => \&_date_min
69             , pattern => \&_pattern
70             , whiteSpace => \&_date_whiteSpace
71             );
72              
73             my %facets_duration =
74             ( enumeration => \&_enumeration
75             , maxExclusive => \&_dur_max_excl
76             , maxInclusive => \&_dur_max_incl
77             , minExclusive => \&_dur_min_excl
78             , minInclusive => \&_dur_min_incl
79             , pattern => \&_pattern
80             , whiteSpace => \&_s_whiteSpace
81             );
82              
83             my $date_time_type = pack_type SCHEMA2001, 'dateTime';
84             my $date_type = pack_type SCHEMA2001, 'date';
85             my $duration_type = pack_type SCHEMA2001, 'duration';
86              
87             sub builtin_facet($$$$$$$$)
88 243     243 1 662 { my ($path, $args, $facet, $value, $is_list, $type, $nss, $action) = @_;
89              
90             my $def
91             = $is_list ? $facets_list{$facet}
92             : $nss->doesExtend($type, $date_time_type) ? $facets_date{$facet}
93             : $nss->doesExtend($type, $date_type) ? $facets_date{$facet}
94             : $nss->doesExtend($type, $duration_type) ? $facets_duration{$facet}
95 243 100       830 : $facets_simple{$facet};
    50          
    100          
    100          
96              
97 243 50       791 $def or error __x"facet {facet} not implemented at {where}"
98             , facet => $facet, where => $path;
99              
100 243         578 $def->($path, $args, $value, $type, $nss, $action);
101             }
102              
103             sub _list_whiteSpace($$$)
104 0     0   0 { my ($path, undef, $ws) = @_;
105 0 0       0 $ws eq 'collapse'
106             or error __x"list whiteSpace facet fixed to 'collapse', not '{ws}' in {path}"
107             , ws => $ws, path => $path;
108 0         0 ();
109             }
110              
111             sub _s_whiteSpace($$$)
112 6     6   14 { my ($path, undef, $ws) = @_;
113 6 50       37 $ws eq 'replace' ? \&_whitespace_replace
    100          
    100          
114             : $ws eq 'collapse' ? \&_whitespace_collapse
115             : $ws eq 'preserve' ? ()
116             : error __x"illegal whiteSpace facet '{ws}' in {path}"
117             , ws => $ws, path => $path;
118             }
119              
120             sub _date_whiteSpace($$$)
121 0     0   0 { my ($path, undef, $ws) = @_;
122              
123             # whitespace processing already in the dateTime parser
124 0 0       0 $ws eq 'collapse'
125             or error __x"illegal whiteSpace facet '{ws}' in {path}"
126             , ws => $ws, path => $path;
127 0         0 ();
128             }
129              
130             sub _whitespace_replace($)
131 2 100   2   10 { my $value = ref $_[0] ? $_[0]->textContent : $_[0];
132 2         17 $value =~ s/[\t\r\n]/ /gs;
133 2         8 $value;
134             }
135              
136             sub _whitespace_collapse($)
137 2 100   2   9 { my $value = ref $_[0] ? $_[0]->textContent : $_[0];
138 2         6 for($value)
139 2         14 { s/[\t\r\n ]+/ /gs;
140 2         12 s/^ +//;
141 2         10 s/ +$//;
142             }
143 2         7 $value;
144             }
145              
146             sub _maybe_big($$$)
147 72     72   134 { my ($path, $args, $value) = @_;
148 72 100       170 return $value if $args->{sloppy_integers};
149              
150             # modules Math::Big* loaded by Schema::Spec when not sloppy
151              
152 70         170 $value =~ s/\s//g;
153 70 50       279 if($value =~ m/[.eE]/)
    50          
154 0         0 { my $c = $value;
155 0 0       0 my $exp = $c =~ s/[eE][+-]?([0-9]+)// ? $1 : 0;
156 0 0       0 my $pre = $c =~ /^[-+]?([0-9]*)/ ? length($1) : 0;
157 0 0 0     0 return Math::BigFloat->new($value)
158             if $pre >= DBL_DIG || $pre+$exp >= DBL_MAX_10_EXP;
159             }
160             # compare ints as strings, because they will overflow!!
161             elsif(substr($value, 0, 1) eq '-')
162 0 0 0     0 { return Math::BigInt->new($value)
      0        
163             if length($value) > length(INT_MIN)
164             || (length($value)==length(INT_MIN) && $value gt INT_MIN);
165             }
166             else
167 70 100 33     306 { return Math::BigInt->new($value)
      66        
168             if length($value) > length(INT_MAX)
169             || (length($value)==length(INT_MAX) && $value gt INT_MAX);
170             }
171              
172 64         170 $value;
173             }
174              
175             sub _s_minInclusive($$$)
176 33     33   84 { my ($path, $args, $min) = @_;
177 33         98 $min = _maybe_big $path, $args, $min;
178 22 100   22   333 sub { return $_[0] if $_[0] >= $min;
179 7         38 error __x"too small inclusive {value}, min {min} at {where}"
180             , value => $_[0], min => $min, where => $path;
181 33         201 };
182             }
183              
184             sub _s_minExclusive($$$)
185 6     6   16 { my ($path, $args, $min) = @_;
186 6         17 $min = _maybe_big $path, $args, $min;
187 5 100   5   22 sub { return $_[0] if $_[0] > $min;
188 2         10 error __x"too small exclusive {value}, larger {min} at {where}"
189             , value => $_[0], min => $min, where => $path;
190 6         36 };
191             }
192              
193             sub _s_maxInclusive($$$)
194 21     21   50 { my ($path, $args, $max) = @_;
195 21         52 $max = _maybe_big $path, $args, $max;
196 19 100   19   212 sub { return $_[0] if $_[0] <= $max;
197 6         112 error __x"too large inclusive {value}, max {max} at {where}"
198             , value => $_[0], max => $max, where => $path;
199 21         486 };
200             }
201              
202             sub _s_maxExclusive($$$)
203 12     12   33 { my ($path, $args, $max) = @_;
204 12         33 $max = _maybe_big $path, $args, $max;
205 4 100   4   15 sub { return $_[0] if $_[0] < $max;
206 2         10 error __x"too large exclusive {value}, smaller {max} at {where}"
207             , value => $_[0], max => $max, where => $path;
208 12         71 };
209             }
210              
211             my $qname = pack_type SCHEMA2001, 'QName';
212             sub _enumeration($$$$$$)
213 62     62   139 { my ($path, $args, $enums, $type, $nss, $action) = @_;
214              
215 62         319 my %enum = map +($_ => 1), @$enums;
216 48 100   48   138 sub { my $v = ref $_[0] eq 'ARRAY' ? join(' ', @{$_[0]}) : $_[0];
  9         33  
217 48 100       210 return $v if exists $enum{$v};
218 18         60 error __x"invalid enumerate `{string}' at {where}"
219             , string => $v, where => $path;
220 62         463 };
221             }
222              
223             sub _s_totalDigits($$$)
224 26     26   64 { my ($path, undef, $total) = @_;
225              
226             # this accidentally also works correctly for NaN +INF -INF
227             sub
228 26     26   46 { my $v = $_[0];
229 26         85 $v =~ s/[eE].*//;
230 26         112 $v =~ s/^[+-]?0*//;
231 26 100       127 return $_[0] if $total >= ($v =~ tr/0-9//);
232              
233 2         9 error __x"decimal too long, got {length} digits max {max} at {where}"
234             , length => ($v =~ tr/0-9//), max => $total, where => $path;
235 26         144 };
236             }
237              
238             sub _s_fractionDigits($$$)
239 2     2   4 { my $frac = $_[2];
240             # can be result from Math::BigFloat, so too long to use %f But rounding
241             # is very hard to implement. If you need this accuracy, then format your
242             # value yourself!
243             sub
244 4     4   9 { my $v = $_[0];
245 4         63 $v =~ s/(\.[0-9]{$frac}).*/$1/;
246 4         320 $v;
247 2         12 };
248             }
249              
250             sub _s_totalFracDigits($$$)
251 3     3   8 { my ($path, undef, $dig) = @_;
252 3         8 my ($total, $frac) = @$dig;
253             sub
254 4     4   9 { my $w = $_[0];
255              
256 4         5 my $v = $w; # total is checking length
257 4         13 $v =~ s/[eE].*//;
258 4         175 $v =~ s/^[+-]?0*//;
259              
260 4 50       320 if( $v !~ /^(?:[+-]?)(?:[0-9]*)(?:\.([0-9]*))?$/ )
261 0         0 { error __x"Invalid numeric format, got {value} at {where}"
262             , value => $w, where => $path;
263             }
264              
265 4 100 66     27 if($1 && length($1) > $frac)
266 1         6 { error __x"fractional part for {value} too long, got {l} digits max {max} at {where}"
267             , value => $w, l => length($1), max => $frac, where => $path;
268             }
269              
270 3 50       19 return $w if $total >= ($v =~ tr/0-9//);
271 0         0 error __x"decimal too long, got {length} digits max {max} at {where}"
272             , length => ($v =~ tr/0-9//), max => $total, where => $path;
273 3         21 };
274             }
275              
276             sub _s_length($$$$$$)
277 19     19   52 { my ($path, $args, $len, $type, $nss, $action) = @_;
278              
279 15 100 66 15   96 sub { return $_[0] if defined $_[0] && length($_[0])==$len;
280 9         41 error __x"string `{string}' does not have required length {len} but {size} at {where}"
281             , string => $_[0], len => $len, size => length($_[0]), where => $path;
282 19         125 };
283             }
284              
285             sub _list_length($$$)
286 6     6   12 { my ($path, $args, $len) = @_;
287 6 100 66 6   15 sub { return $_[0] if defined $_[0] && @{$_[0]}==$len;
  6         20  
288 4         15 error __x"list `{list}' does not have required length {len} at {where}"
289             , list => $_[0], len => $len, where => $path;
290 6         40 };
291             }
292              
293             sub _s_minLength($$$)
294 8     8   21 { my ($path, $args, $len, $type, $nss, $action) = @_;
295              
296 4 50 33 4   33 sub { return $_[0] if defined $_[0] && length($_[0]) >=$len;
297 0         0 error __x"string `{string}' does not have minimum length {len} at {where}"
298             , string => $_[0], len => $len, where => $path;
299 8         49 };
300             }
301              
302             sub _list_minLength($$$)
303 0     0   0 { my ($path, $args, $len) = @_;
304 0 0 0 0   0 sub { return $_[0] if defined $_[0] && @{$_[0]} >=$len;
  0         0  
305 0         0 error __x"list `{list}' does not have minimum length {len} at {where}"
306             , list => $_[0], len => $len, where => $path;
307 0         0 };
308             }
309              
310             sub _s_maxLength($$$)
311 0     0   0 { my ($path, $args, $len, $type, $nss, $action) = @_;
312              
313 0 0 0 0   0 sub { return $_[0] if defined $_[0] && length $_[0] <= $len;
314 0         0 error __x"string `{string}' longer than maximum length {len} at {where}"
315             , string => $_[0], len => $len, where => $path;
316 0         0 };
317             }
318              
319             sub _list_maxLength($$$)
320 0     0   0 { my ($path, $args, $len) = @_;
321 0 0 0 0   0 sub { return $_[0] if defined $_[0] && @{$_[0]} <= $len;
  0         0  
322 0         0 error __x"list `{list}' longer than maximum length {len} at {where}"
323             , list => $_[0], len => $len, where => $path;
324 0         0 };
325             }
326              
327             sub _pattern($$$)
328 30     30   62 { my ($path, $args, $pats) = @_;
329 30 50       64 @$pats or return ();
330 30 50       73 my $regex = @$pats==1 ? $pats->[0] : "(".join(')|(', @$pats).")";
331 30         119 my $compiled = XML::LibXML::RegExp->new($regex);
332              
333             sub {
334 50     50   465 use Carp 'cluck';
  50         111  
  50         43883  
335 26 50   26   65 defined $_[0] or cluck "PATTERN";
336 26 100       73 my $v = ref $_[0] ? $_[0]->textContent : $_[0];
337 26 100       213 return $_[0] if $compiled->matches($v);
338 6         25 error __x"string `{string}' does not match pattern `{pat}' at {where}"
339             , string => $v, pat => $regex, where => $path;
340 30         933 };
341             }
342              
343             sub _date_min($$$)
344 2     2   5 { my ($path, $args, $min) = @_;
345 2 50   2   12 sub { return $_[0] if $_[0] gt $min;
346 0         0 error __x"too small inclusive {value}, min {min} at {where}"
347             , value => $_[0], min => $min, where => $path;
348 2         14 };
349             }
350              
351             sub _date_max($$$)
352 2     2   14 { my ($path, $args, $max) = @_;
353 2 50   2   8 sub { return $_[0] if $_[0] lt $max;
354 0         0 error __x"too large inclusive {value}, max {max} at {where}"
355             , value => $_[0], max => $max, where => $path;
356 2         17 };
357             }
358              
359             sub _date_expl_tz($$$)
360 0     0   0 { my ($path, $args, $enum) = @_;
361 0         0 my $tz = qr/Z$ | [+-](?:(?:0[0-9]|1[0-3])\:[0-5][0-9] | 14\:00)$/x;
362              
363             $enum eq 'optional' ? ()
364             : $enum eq 'prohibited'
365 0 0   0   0 ? sub { $_[0] !~ $tz
366             or error __x"timezone forbidden on {date} at {where}"
367             , date => $_[0], where => $path;
368             }
369             : $enum eq 'required'
370 0 0   0   0 ? sub { $_[0] =~ $tz
371             or error __x"timezone required on {date} at {where}"
372             , date => $_[0], where => $path;
373             }
374 0 0       0 : error __x"illegal explicitTimeZone facet '{enum}' in {path}"
    0          
    0          
375             , enum => $enum, path => $path;
376             }
377              
378             sub _dur_min_incl($$$)
379 5     5   13 { my ($path, $args, $min) = @_;
380 5         13 my $secs = duration2secs $min;
381              
382 5 100   5   18 sub { return $_[0] if duration2secs $_[0] >= $secs;
383 1         6 error __x"too small minInclusive duration {value}, min {min} at {where}"
384             , value => $_[0], min => $min, where => $path;
385 5         46 };
386             }
387              
388              
389             sub _dur_min_excl($$$)
390 0     0     { my ($path, $args, $min) = @_;
391 0           my $secs = duration2secs $min;
392              
393 0 0   0     sub { return $_[0] if duration2secs $_[0] > $secs;
394 0           error __x"too small minExclusive duration {value}, min {min} at {where}"
395             , value => $_[0], min => $min, where => $path;
396 0           };
397             }
398              
399              
400             sub _dur_max_incl($$$)
401 0     0     { my ($path, $args, $max) = @_;
402 0           my $secs = duration2secs $max;
403              
404 0 0   0     sub { return $_[0] if duration2secs $_[0] <= $secs;
405 0           error __x"too large maxInclusive duration {value}, max {max} at {where}"
406             , value => $_[0], max => $max, where => $path;
407 0           };
408             }
409              
410              
411             sub _dur_max_excl($$$)
412 0     0     { my ($path, $args, $max) = @_;
413 0           my $secs = duration2secs $max;
414              
415 0 0   0     sub { return $_[0] if duration2secs $_[0] < $secs;
416 0           error __x"too large maxExclusive duration {value}, max {max} at {where}"
417             , value => $_[0], max => $max, where => $path;
418 0           };
419             }
420              
421             1;