File Coverage

blib/lib/XML/Validator/Schema/SimpleType.pm
Criterion Covered Total %
statement 121 145 83.4
branch 40 68 58.8
condition 10 16 62.5
subroutine 24 24 100.0
pod 0 6 0.0
total 195 259 75.2


line stmt bran cond sub pod time code
1             package XML::Validator::Schema::SimpleType;
2 6     6   42 use strict;
  6         10  
  6         193  
3 6     6   28 use warnings;
  6         13  
  6         190  
4              
5              
6             =item NAME
7              
8             XML::Validator::Schema::SimpleType
9              
10             =head1 DESCRIPTION
11              
12             XML Schema simple type system. This module provides objects and class
13             methods to support simple types. For complex types see the ModelNode
14             class.
15              
16             =head1 USAGE
17              
18             # create a new anonymous type based on an existing type
19             my $type = $string->derive();
20              
21             # create a new named type based on an existing type
22             my $type = $string->derive(name => 'myString');
23              
24             # add a restriction
25             $type->restrict(enumeration => "10");
26              
27             # check a value against a type
28             ($ok, $msg) = $type->check($value);
29              
30             =cut
31              
32 6     6   30 use Carp qw(croak);
  6         8  
  6         306  
33 6     6   30 use XML::Validator::Schema::Util qw(XSD _err);
  6         9  
  6         284  
34              
35             # facet support bit-patterns
36 6     6   27 use constant LENGTH => 0b0000000000000001;
  6         9  
  6         385  
37 6     6   28 use constant MINLENGTH => 0b0000000000000010;
  6         11  
  6         293  
38 6     6   34 use constant MAXLENGTH => 0b0000000000000100;
  6         10  
  6         255  
39 6     6   26 use constant PATTERN => 0b0000000000001000;
  6         10  
  6         260  
40 6     6   32 use constant ENUMERATION => 0b0000000000010000;
  6         9  
  6         221  
41 6     6   27 use constant WHITESPACE => 0b0000000000100000;
  6         38  
  6         310  
42 6     6   26 use constant MAXINCLUSIVE => 0b0000000001000000;
  6         16  
  6         324  
43 6     6   33 use constant MAXEXCLUSIVE => 0b0000000010000000;
  6         9  
  6         245  
44 6     6   27 use constant MININCLUSIVE => 0b0000000100000000;
  6         56  
  6         242  
45 6     6   26 use constant MINEXCLUSIVE => 0b0000001000000000;
  6         10  
  6         255  
46 6     6   27 use constant TOTALDIGITS => 0b0000010000000000;
  6         10  
  6         329  
47 6     6   27 use constant FRACTIONDIGITS => 0b0000100000000000;
  6         11  
  6         31853  
48              
49             # hash mapping names to values
50             our %FACET = (length => LENGTH,
51             minLength => MINLENGTH,
52             maxLength => MAXLENGTH,
53             pattern => PATTERN,
54             enumeration => ENUMERATION,
55             whiteSpace => WHITESPACE,
56             maxInclusive => MAXINCLUSIVE,
57             maxExclusive => MAXEXCLUSIVE,
58             minInclusive => MININCLUSIVE,
59             minExclusive => MINEXCLUSIVE,
60             totalDigits => TOTALDIGITS,
61             fractionDigits => FRACTIONDIGITS);
62              
63             # initialize builtin types
64             our %BUILTIN;
65              
66             # create the primitive types
67             $BUILTIN{string} = __PACKAGE__->new(name => 'string',
68             facets => LENGTH|MINLENGTH|MAXLENGTH|
69             PATTERN|ENUMERATION|WHITESPACE,
70             );
71              
72             $BUILTIN{boolean} = __PACKAGE__->new(name => 'boolean',
73             facets => PATTERN|WHITESPACE,
74             );
75             $BUILTIN{boolean}->restrict(enumeration => "1",
76             enumeration => "0",
77             enumeration => "true",
78             enumeration => "false");
79              
80             $BUILTIN{decimal} = __PACKAGE__->new(name => 'decimal',
81             facets => TOTALDIGITS|FRACTIONDIGITS|
82             PATTERN|WHITESPACE|
83             #ENUMERATION|
84             MAXINCLUSIVE|MAXEXCLUSIVE|
85             MININCLUSIVE|MINEXCLUSIVE,
86             );
87             $BUILTIN{decimal}->restrict(pattern => qr/^[+-]?(?:(?:\d+(?:\.\d+)?)|(?:\.\d+))$/);
88              
89             $BUILTIN{dateTime} = __PACKAGE__->new(name => 'dateTime',
90             facets => PATTERN|WHITESPACE
91             #|ENUMERATION|
92             #MAXINCLUSIVE|MAXEXCLUSIVE|
93             #MININCLUSIVE|MINEXCLUSIVE,
94             );
95             $BUILTIN{dateTime}->restrict(pattern => qr/^[-+]?(\d{4,})-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:(?:Z)|(?:[-+]\d{2}:\d{2}))?$/);
96              
97             $BUILTIN{float} = __PACKAGE__->new(name => 'float',
98             facets => PATTERN|WHITESPACE,
99             #|ENUMERATION|
100             #MAXINCLUSIVE|MAXEXCLUSIVE|
101             #MININCLUSIVE|MINEXCLUSIVE);
102             );
103              
104             $BUILTIN{float}->restrict(pattern =>
105             qr/^[+-]?(?:(?:INF)|(?:NaN)|(?:\d+(?:\.\d+)?)(?:[eE][+-]?\d+)?)$/);
106              
107             $BUILTIN{double} = __PACKAGE__->new(name => 'double',
108             facets => PATTERN|WHITESPACE,
109             #|ENUMERATION|
110             #MAXINCLUSIVE|MAXEXCLUSIVE|
111             #MININCLUSIVE|MINEXCLUSIVE);
112             );
113              
114             $BUILTIN{double}->restrict(pattern =>
115             qr/^[+-]?(?:(?:INF)|(?:NaN)|(?:\d+(?:\.\d+)?)(?:[eE][+-]?\d+)?)$/);
116              
117             $BUILTIN{duration} = __PACKAGE__->new(name => 'duration',
118             facets => PATTERN|WHITESPACE,);
119             #facets => PATTERN|WHITESPACE|ENUMERATION|MAXINCLUSIVE|MAXEXCLUSIVE|MININCLUSIVE|MINEXCLUSIVE);
120            
121             # thanks to perlmonk Abigail-II
122             $BUILTIN{duration}->restrict(pattern => qr /^-? # Optional leading minus.
123             P # Required.
124             (?=[T\d]) # Duration cannot be empty.
125             (?:(?!-) \d+ Y)? # Non-negative integer, Y (optional)
126             (?:(?!-) \d+ M)? # Non-negative integer, M (optional)
127             (?:(?!-) \d+ D)? # Non-negative integer, D (optional)
128             (
129             (?:T (?=\d) # T, must be followed by a digit.
130             (?:(?!-) \d+ H)? # Non-negative integer, H (optional)
131             (?:(?!-) \d+ M)? # Non-negative integer, M (optional)
132             (?:(?!-) \d+\.\d+ S)? # Non-negative decimal, S (optional)
133             )? # Entire T part is optional
134             )$/x);
135              
136             $BUILTIN{time} = __PACKAGE__->new(name => 'time',
137             facets => PATTERN|WHITESPACE
138             #|ENUMERATION|
139             #MAXINCLUSIVE|MAXEXCLUSIVE|
140             #MININCLUSIVE|MINEXCLUSIVE,
141             );
142             $BUILTIN{time}->restrict(pattern =>
143             qr /^[0-2]\d:[0-5]\d:[0-5]\d(\.\d+)?(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/);
144              
145             $BUILTIN{date} = __PACKAGE__->new(name => 'date',
146             facets => PATTERN|WHITESPACE
147             #|ENUMERATION|
148             #MAXINCLUSIVE|MAXEXCLUSIVE|
149             #MININCLUSIVE|MINEXCLUSIVE,
150             );
151             $BUILTIN{date}->restrict(pattern =>
152             qr /^[-]?(\d{4,})-(\d\d)-(\d\d)(??{ _validate_date($1,$2,$3) })(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/);
153            
154              
155             $BUILTIN{gYearMonth} = __PACKAGE__->new(name => 'gYearMonth',
156             facets => PATTERN|WHITESPACE
157             #|ENUMERATION|
158             #MAXINCLUSIVE|MAXEXCLUSIVE|
159             #MININCLUSIVE|MINEXCLUSIVE,
160             );
161             $BUILTIN{gYearMonth}->restrict(pattern =>
162             qr /^[-]?(\d{4,})-(1[0-2]{1}|0\d{1})(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/);
163              
164             $BUILTIN{gYear} = __PACKAGE__->new(name => 'gYear',
165             facets => PATTERN|WHITESPACE
166             #|ENUMERATION|
167             #MAXINCLUSIVE|MAXEXCLUSIVE|
168             #MININCLUSIVE|MINEXCLUSIVE,
169             );
170             $BUILTIN{gYear}->restrict(pattern =>
171             qr /^[-]?(\d{4,})(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/);
172              
173             $BUILTIN{gMonthDay} = __PACKAGE__->new(name => 'gMonthDay',
174             facets => PATTERN|WHITESPACE
175             #|ENUMERATION|
176             #MAXINCLUSIVE|MAXEXCLUSIVE|
177             #MININCLUSIVE|MINEXCLUSIVE,
178             );
179             $BUILTIN{gMonthDay}->restrict(pattern =>
180             qr /^--(\d{2,})-(\d\d)(??{_validate_date(1999,$1,$2)})(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/ );
181              
182             $BUILTIN{gDay} = __PACKAGE__->new(name => 'gDay',
183             facets => PATTERN|WHITESPACE
184             #|ENUMERATION|
185             #MAXINCLUSIVE|MAXEXCLUSIVE|
186             #MININCLUSIVE|MINEXCLUSIVE,
187             );
188             $BUILTIN{gDay}->restrict(pattern =>
189             qr /^---([0-2]\d{1}|3[0|1])(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/ );
190              
191             $BUILTIN{gMonth} = __PACKAGE__->new(name => 'gMonth',
192             facets => PATTERN|WHITESPACE
193             #|ENUMERATION|
194             #MAXINCLUSIVE|MAXEXCLUSIVE|
195             #MININCLUSIVE|MINEXCLUSIVE,
196             );
197             $BUILTIN{gMonth}->restrict(pattern =>
198             qr /^--(0\d|1[0-2])(Z?|([+|-]([0-1]\d|2[0-4])\:([0-5]\d))?)$/ );
199              
200             $BUILTIN{hexBinary} = __PACKAGE__->new(name => 'hexBinary',
201             facets => PATTERN|WHITESPACE
202             #|ENUMERATION|
203             #MAXINCLUSIVE|MAXEXCLUSIVE|
204             #MININCLUSIVE|MINEXCLUSIVE,
205             );
206             $BUILTIN{hexBinary}->restrict(pattern =>
207             qr /^([0-9a-fA-F][0-9a-fA-F])+$/);
208              
209              
210             $BUILTIN{base64Binary} = __PACKAGE__->new(name => 'base64Binary',
211             facets => PATTERN|WHITESPACE
212             #|ENUMERATION|
213             #MAXINCLUSIVE|MAXEXCLUSIVE|
214             #MININCLUSIVE|MINEXCLUSIVE,
215             );
216             $BUILTIN{base64Binary}->restrict(pattern =>
217             qr /^([0-9a-zA-Z\+\\\=][0-9a-zA-Z\+\\\=])+$/);
218              
219             $BUILTIN{anyURI} = __PACKAGE__->new(name => 'anyURI',
220             facets => LENGTH|MINLENGTH|MAXLENGTH|
221             PATTERN|ENUMERATION|WHITESPACE,
222             );
223              
224             $BUILTIN{QName} = __PACKAGE__->new(name => 'QName',
225             facets => PATTERN|WHITESPACE
226             #|ENUMERATION|
227             #MAXINCLUSIVE|MAXEXCLUSIVE|
228             #MININCLUSIVE|MINEXCLUSIVE,
229             );
230             $BUILTIN{QName}->restrict(pattern =>
231             qr /^([A-z][A-z0-9]+:)?([A-z][A-z0-9]+)$/);
232              
233             $BUILTIN{NOTATION} = __PACKAGE__->new(name => 'NOTATION',
234             facets => PATTERN|WHITESPACE
235             #|ENUMERATION|
236             #MAXINCLUSIVE|MAXEXCLUSIVE|
237             #MININCLUSIVE|MINEXCLUSIVE,
238             );
239             $BUILTIN{NOTATION}->restrict(pattern =>
240             qr /^([A-z][A-z0-9]+:)?([A-z][A-z0-9]+)$/);
241              
242             # create derived types
243             $BUILTIN{integer} = $BUILTIN{decimal}->derive(name => 'integer');
244             $BUILTIN{integer}->restrict(pattern => qr/^[+-]?\d+$/);
245              
246             # http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/#nonPositiveInteger
247             $BUILTIN{nonPositiveInteger} = $BUILTIN{integer}->derive(name => 'nonPositiveInteger');
248             $BUILTIN{nonPositiveInteger}->restrict( maxInclusive => 0 );
249              
250             # http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/#nonNegativeInteger
251             $BUILTIN{nonNegativeInteger} = $BUILTIN{integer}->derive(name => 'nonNegativeInteger');
252             $BUILTIN{nonNegativeInteger}->restrict( minInclusive => 0 );
253              
254             # http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/#positiveInteger
255             $BUILTIN{positiveInteger} = $BUILTIN{nonNegativeInteger}->derive(name => 'positiveInteger');
256             $BUILTIN{positiveInteger}->restrict( minInclusive => 1 );
257              
258             # http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/#negativeInteger
259             $BUILTIN{negativeInteger} = $BUILTIN{nonPositiveInteger}->derive(name => 'negativeInteger');
260             $BUILTIN{negativeInteger}->restrict( maxInclusive => -1 );
261              
262             $BUILTIN{int} = $BUILTIN{integer}->derive(name => 'int');
263             $BUILTIN{int}->restrict(minInclusive => -2147483648,
264             maxInclusive => 2147483647);
265              
266             $BUILTIN{unsignedInt} = $BUILTIN{integer}->derive(name => 'unsignedInt');
267             $BUILTIN{unsignedInt}->restrict(minInclusive => 0,
268             maxInclusive => 4294967295);
269              
270             $BUILTIN{short} = $BUILTIN{int}->derive(name => 'short');
271             $BUILTIN{short}->restrict(minInclusive => -32768,
272             maxInclusive => 32767);
273              
274             $BUILTIN{unsignedShort} = $BUILTIN{unsignedInt}->derive(name =>
275             'unsignedShort');
276             $BUILTIN{unsignedShort}->restrict(maxInclusive => 65535);
277              
278             $BUILTIN{byte} = $BUILTIN{short}->derive(name => 'byte');
279             $BUILTIN{byte}->restrict(minInclusive => -128,
280             maxInclusive => 127);
281              
282             $BUILTIN{unsignedByte} = $BUILTIN{unsignedShort}->derive(name =>
283             'unsignedByte');
284             $BUILTIN{unsignedByte}->restrict(maxInclusive => 255);
285              
286             $BUILTIN{normalizedString} = $BUILTIN{string}->derive(name =>
287             'normalizedString');
288             $BUILTIN{normalizedString}->restrict(whiteSpace => 'replace');
289              
290             $BUILTIN{token} = $BUILTIN{normalizedString}->derive(name => 'token');
291             $BUILTIN{token}->restrict(whiteSpace => 'collapse');
292              
293             $BUILTIN{NMTOKEN} = $BUILTIN{token}->derive(name => 'NMTOKEN');
294             $BUILTIN{NMTOKEN}->restrict(pattern => qr/^[-.:\w\d]*$/);
295              
296             ######################
297             # SimpleType methods #
298             ######################
299              
300             # create a new type, filing in the library if named
301             sub new {
302 198     198 0 475 my ($pkg, %arg) = @_;
303 198         528 my $self = bless(\%arg, $pkg);
304              
305 198         451 return $self;
306             }
307              
308             # create a type derived from this type
309             sub derive {
310 84     84 0 168 my ($self, @opt) = @_;
311              
312 84         234 my $sub = ref($self)->new(@opt);
313 84         137 $sub->{base} = $self;
314              
315 84         190 return $sub;
316             }
317              
318             sub restrict {
319 186     186 0 279 my $self = shift;
320 186         383 my $root = $self->root;
321              
322 186         378 while (@_) {
323 228         335 my ($key, $value) = (shift, shift);
324            
325            
326             # is this a legal restriction? (base types can do whatever they want
327 228 50 66     857 _err("Found illegal restriction '$key' on type derived from '$root->{name}'.")
328             unless ($self == $root) or
329             ($FACET{$key} & $root->{facets});
330              
331 228   100     251 push @{$self->{restrict}{$key} ||= []}, $value;
  228         1874  
332             }
333             }
334              
335             # returns the ultimate base type for this type
336             sub root {
337 529     529 0 617 my $self = shift;
338 529         527 my $p = $self;
339 529         1211 while ($p->{base}) {
340 537         1121 $p = $p->{base};
341             }
342 529         840 return $p;
343             }
344              
345             sub normalize_ws {
346 644     644 0 694 my ($self, $value) = @_;
347            
348 644 100       1302 if ($self->{restrict}{whiteSpace}) {
349 18         25 my $ws = $self->{restrict}{whiteSpace}[0];
350 18 100       39 if ($ws eq 'replace') {
    50          
351 10         22 $value =~ s![\t\n\r]! !g;
352             } elsif ($ws eq 'collapse') {
353 8         12 $value =~ s!\s+! !g;
354 8         10 $value =~ s!^\s!!g;
355 8         13 $value =~ s!\s$!!g;
356             }
357 18         33 return $value;
358             }
359 626 100       1295 return $self->{base}->normalize_ws($value) if $self->{base};
360 325         594 return $value;
361             }
362              
363             sub check {
364 343     343 0 965 my ($self, $value) = @_;
365 343         646 my $root = $self->root;
366 343         375 my ($ok, $msg);
367              
368             # first deal with whitespace, necessary before applying facets
369 343         575 $value = $self->normalize_ws($value);
370              
371             # first check base restrictions
372 343 100       691 if ($self->{base}) {
373 175         349 ($ok, $msg) = $self->{base}->check($value);
374 175 100       505 return ($ok, $msg) unless $ok;
375             }
376              
377             # check various constraints
378 331         387 my $r = $self->{restrict};
379              
380 331 50       534 if ($r->{length}) {
381 0         0 foreach my $len (@{$r->{length}}) {
  0         0  
382 0 0       0 return (0, "is not exactly $len characters.")
383             unless length($value) eq $len;
384             }
385             }
386              
387 331 50       1808 if ($r->{maxLength}) {
388 0         0 foreach my $len (@{$r->{maxLength}}) {
  0         0  
389 0 0       0 return (0, "is longer than maximum $len characters.")
390             if length($value) > $len;
391             }
392             }
393              
394 331 50       715 if ($r->{minLength}) {
395 0         0 foreach my $len (@{$r->{minLength}}) {
  0         0  
396 0 0       0 return (0, "is shorter than minimum $len characters.")
397             if length($value) < $len;
398             }
399             }
400              
401 331 100       653 if ($r->{enumeration}) {
402 1         9 return (0, 'not in allowed list (' .
403 20         42 join(', ', @{$r->{enumeration}}) . ')')
404 5 100       8 unless grep { $_ eq $value } (@{$r->{enumeration}});
  5         12  
405             }
406              
407 330 100       765 if ($r->{pattern}) {
408 218         230 my $pass = 0;
409 218         206 foreach my $pattern (@{$r->{pattern}}) {
  218         416  
410 218 100       1583 if ($value =~ /$pattern/) {
411 183         388 $pass = 1;
412 183         238 last;
413             }
414             }
415 218 100       499 return (0, "does not match required pattern.")
416             unless $pass;
417             }
418              
419 295 100       514 if ($r->{minInclusive}) {
420 58         59 foreach my $min (@{$r->{minInclusive}}) {
  58         96  
421 58 100       226 return (0, "is below minimum (inclusive) allowed, $min")
422             if $value < $min;
423             }
424             }
425              
426 278 50       530 if ($r->{minExclusive}) {
427 0         0 foreach my $min (@{$r->{minExclusive}}) {
  0         0  
428 0 0       0 return (0, "is below minimum allowed, $min")
429             if $value <= $min;
430             }
431             }
432              
433 278 100       483 if ($r->{maxInclusive}) {
434 54         55 foreach my $max (@{$r->{maxInclusive}}) {
  54         86  
435 54 100       176 return (0, "is above maximum (inclusive) allowed, $max")
436             if $value > $max;
437             }
438             }
439              
440 266 50       448 if ($r->{maxExclusive}) {
441 0         0 foreach my $max (@{$r->{maxExclusive}}) {
  0         0  
442 0 0       0 return (0, "is above maximum allowed, $max")
443             if $value >= $max;
444             }
445             }
446              
447 266 50 33     1094 if ($r->{totalDigits} or $r->{fractionDigits}) {
448             # strip leading and trailing zeros for numeric constraints
449 0         0 (my $digits = $value) =~ s/^([+-]?)0*(\d*\.?\d*?)0*$/$1$2/g;
450              
451 0 0       0 if ($r->{totalDigits}) {
452 0         0 foreach my $tdigits (@{$r->{totalDigits}}) {
  0         0  
453 0 0       0 return (0, "has more total digits than allowed, $tdigits")
454             if $digits =~ tr!0-9!! > $tdigits;
455             }
456             }
457              
458 0 0       0 if ($r->{fractionDigits}) {
459 0         0 foreach my $fdigits (@{$r->{fractionDigits}}) {
  0         0  
460 0 0       0 return (0, "has more fraction digits than allowed, $fdigits")
461             if $digits =~ /\.\d{$fdigits}\d/;
462             }
463             }
464             }
465              
466 266         558 return (1);
467             }
468              
469             #
470             # begin code taken from Date::Simple
471             #
472              
473             my @days_in_month = ([0,31,28,31,30,31,30,31,31,30,31,30,31],
474             [0,31,29,31,30,31,30,31,31,30,31,30,31]);
475              
476             sub _validate_date {
477 10     10   29 my ($y, $m, $d)= @_;
478              
479             # any +ve integral year is valid
480 10 50       29 return q{(?!)} if $y != abs int $y;
481 10 50 33     43 return q{(?!)} unless 1 <= $m and $m <= 12;
482 10 100 66     38 return q{(?!)} unless 1 <= $d and $d <=$days_in_month[_leap_year($y)][$m];
483              
484             # perl 5.10.0 choked on (?=) here, switching to just returning
485             # nothing, which should also always match.
486 9         115 return '';
487             }
488              
489             sub _leap_year {
490 10     10   13 my $y = shift;
491 10   100     124 return (($y%4==0) and ($y%400==0 or $y%100!=0)) || 0;
492             }
493              
494             #
495             # end code taken from Date::Simple
496             #
497              
498             1;