File Coverage

blib/lib/JSON/Schema/Modern/Vocabulary/Validation.pm
Criterion Covered Total %
statement 361 361 100.0
branch 142 166 85.5
condition 98 125 78.4
subroutine 68 68 100.0
pod 0 3 0.0
total 669 723 92.5


line stmt bran cond sub pod time code
1 31     31   18504 use strict;
  31         94  
  31         1082  
2 31     31   221 use warnings;
  31         128  
  31         1816  
3             package JSON::Schema::Modern::Vocabulary::Validation;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Validation vocabulary
6              
7             our $VERSION = '0.570';
8              
9 31     31   597 use 5.020;
  31         129  
10 31     31   210 use Moo;
  31         82  
  31         227  
11 31     31   11493 use strictures 2;
  31         274  
  31         1190  
12 31     31   5965 use stable 0.031 'postderef';
  31         567  
  31         198  
13 31     31   4769 use experimental 'signatures';
  31         110  
  31         161  
14 31     31   2810 use if "$]" >= 5.022, experimental => 're_strict';
  31         96  
  31         330  
15 31     31   3003 no if "$]" >= 5.031009, feature => 'indirect';
  31         76  
  31         260  
16 31     31   1757 no if "$]" >= 5.033001, feature => 'multidimensional';
  31         99  
  31         241  
17 31     31   1600 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  31         188  
  31         265  
18 31     31   1335 use List::Util 'any';
  31         113  
  31         2602  
19 31     31   270 use Ref::Util 0.100 'is_plain_arrayref';
  31         595  
  31         1720  
20 31     31   220 use Scalar::Util 'looks_like_number';
  31         109  
  31         2713  
21 31     31   231 use if "$]" >= 5.022, POSIX => 'isinf';
  31         126  
  31         259  
22 31     31   59700 use JSON::Schema::Modern::Utilities qw(is_type get_type is_equal is_elements_unique E assert_keyword_type assert_pattern jsonp sprintf_num);
  31         95  
  31         2830  
23 31     31   32697 use Math::BigFloat;
  31         1635346  
  31         213  
24 31     31   693076 use namespace::clean;
  31         99  
  31         368  
25              
26             with 'JSON::Schema::Modern::Vocabulary';
27              
28             sub vocabulary {
29 15     15 0 92 'https://json-schema.org/draft/2019-09/vocab/validation' => 'draft2019-09',
30             'https://json-schema.org/draft/2020-12/vocab/validation' => 'draft2020-12';
31             }
32              
33 6     6 0 30 sub evaluation_order { 1 }
34              
35 90     90 0 216 sub keywords ($self, $spec_version) {
  90         195  
  90         194  
  90         162  
36             return (
37 90 100       6356 qw(type enum const
    100          
38             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
39             maxLength minLength pattern
40             maxItems minItems uniqueItems),
41             $spec_version ne 'draft7' ? qw(maxContains minContains) : (),
42             qw(maxProperties minProperties required),
43             $spec_version ne 'draft7' ? 'dependentRequired' : (),
44             );
45             }
46              
47 5947     5947   10701 sub _traverse_keyword_type ($self, $schema, $state) {
  5947         10135  
  5947         9488  
  5947         9073  
  5947         9654  
48 5947 100       15509 if (is_plain_arrayref($schema->{type})) {
49 283 50       952 return E($state, 'type array is empty') if not $schema->{type}->@*;
50 283         762 foreach my $type ($schema->{type}->@*) {
51             return E($state, 'unrecognized type "%s"', $type//'<null>')
52 584 100 50 2163   2574 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2163   100     5684  
53             }
54 281 50       1228 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
55             }
56             else {
57 5664 100       16641 return if not assert_keyword_type($state, $schema, 'string');
58             return E($state, 'unrecognized type "%s"', $schema->{type}//'<null>')
59 5662 100 50 26221   34975 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  26221   50     60839  
60             }
61 5939         27976 return 1;
62             }
63              
64 6941     6941   11663 sub _eval_keyword_type ($self, $data, $schema, $state) {
  6941         11873  
  6941         11992  
  6941         11244  
  6941         10661  
  6941         10667  
65 6941         19338 my $type = get_type($data);
66 6941 100       26500 if (is_plain_arrayref($schema->{type})) {
67             return 1 if any {
68             $type eq $_ or ($_ eq 'number' and $type eq 'integer')
69             or ($type eq 'string' and $state->{stringy_numbers} and looks_like_number($data)
70             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
71 1701 50 100 1701   15580 or ($_ eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR')
      100        
      66        
      66        
      100        
      100        
      66        
      66        
      33        
72 1525 100       10730 } $schema->{type}->@*;
73 115         1078 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
74             }
75             else {
76             return 1 if $type eq $schema->{type} or ($schema->{type} eq 'number' and $type eq 'integer')
77             or ($type eq 'string' and $state->{stringy_numbers} and looks_like_number($data)
78             and ($schema->{type} eq 'number' or ($schema->{type} eq 'integer' and $data == int($data))))
79 5416 100 100     37634 or ($schema->{type} eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR');
      100        
      100        
      100        
      66        
      66        
      66        
      100        
      100        
      66        
80 1411         4963 return E($state, 'got %s, not %s', $type, $schema->{type});
81             }
82             }
83              
84 499     499   885 sub _traverse_keyword_enum ($self, $schema, $state) {
  499         883  
  499         779  
  499         845  
  499         765  
85 499 50       1412 return if not assert_keyword_type($state, $schema, 'array');
86 499         1441 return 1;
87             }
88              
89 456     456   942 sub _eval_keyword_enum ($self, $data, $schema, $state) {
  456         867  
  456         838  
  456         716  
  456         733  
  456         724  
90 456         1120 my @s; my $idx = 0;
  456         765  
91 456         1470 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
92 456 100   1117   3333 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1117         12794  
93             return E($state, 'value does not match'
94             .(!(grep $_->{path}, @s) ? ''
95 188 100       1780 : ' (differences start '.join(', ', map 'from item #'.$_.' at "'.$s[$_]->{path}.'"', 0..$#s).')'));
96             }
97              
98 1225     1225   3409 sub _traverse_keyword_const { 1 }
99              
100 1111     1111   2102 sub _eval_keyword_const ($self, $data, $schema, $state) {
  1111         1930  
  1111         2043  
  1111         1739  
  1111         1768  
  1111         1789  
101 1111         3119 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
102 1111 100       4911 return 1 if is_equal($data, $schema->{const}, my $s = { scalarref_booleans => $state->{scalarref_booleans} });
103             return E($state, 'value does not match'
104 522 100       6175 .($s->{path} ? ' (differences start at "'.$s->{path}.'")' : ''));
105             }
106              
107 903     903   1673 sub _traverse_keyword_multipleOf ($self, $schema, $state) {
  903         1608  
  903         1513  
  903         1439  
  903         1375  
108 903 100       2591 return if not assert_keyword_type($state, $schema, 'number');
109 901 50       2773 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
110 901         14188 return 1;
111             }
112              
113 906     906   2040 sub _eval_keyword_multipleOf ($self, $data, $schema, $state) {
  906         1681  
  906         1633  
  906         1484  
  906         1435  
  906         1457  
114             return 1 if not is_type('number', $data)
115             and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data)
116 906 50 66     2751 and do { $data = 0+$data; 1 });
  2   66     10  
  2   33     9  
      100        
117              
118             # if either value is a float, use the bignum library for the calculation for an accurate remainder
119 700 100 100     4366 if (ref($data) =~ /^Math::Big(?:Int|Float)$/
      66        
      100        
120             or ref($schema->{multipleOf}) =~ /^Math::Big(?:Int|Float)$/
121             or get_type($data) eq 'number' or get_type($schema->{multipleOf}) eq 'number') {
122 52 100       345 $data = ref($data) =~ /^Math::Big(?:Int|Float)$/ ? $data->copy : Math::BigFloat->new($data);
123 52 100       3792 my $divisor = ref($schema->{multipleOf}) =~ /^Math::Big(?:Int|Float)$/ ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
124 52         1433 my ($quotient, $remainder) = $data->bdiv($divisor);
125 52 50       59010 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
126 52 100       612 return 1 if $remainder == 0;
127             }
128             else {
129 648         1974 my $quotient = $data / $schema->{multipleOf};
130 648 50       4200 return E($state, 'overflow while calculating quotient of integers')
    50          
131             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
132 648 100       2860 return 1 if int($quotient) == $quotient;
133             }
134              
135 326         6366 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
136             }
137              
138 769     769   3443 sub _traverse_keyword_maximum { goto \&_assert_number }
139              
140 691     691   1352 sub _eval_keyword_maximum ($self, $data, $schema, $state) {
  691         1404  
  691         1263  
  691         1197  
  691         1168  
  691         1115  
141             return 1 if not is_type('number', $data)
142 691 50 66     2194 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
143 480 100       2668 return 1 if 0+$data <= $schema->{maximum};
144 214         8078 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
145             }
146              
147 550     550   2272 sub _traverse_keyword_exclusiveMaximum { goto \&_assert_number }
148              
149 514     514   960 sub _eval_keyword_exclusiveMaximum ($self, $data, $schema, $state) {
  514         940  
  514         1206  
  514         818  
  514         854  
  514         768  
150             return 1 if not is_type('number', $data)
151 514 50 66     1546 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
152 309 100       1370 return 1 if 0+$data < $schema->{exclusiveMaximum};
153 163         10812 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
154             }
155              
156 787     787   3257 sub _traverse_keyword_minimum { goto \&_assert_number }
157              
158 767     767   1469 sub _eval_keyword_minimum ($self, $data, $schema, $state) {
  767         1469  
  767         1325  
  767         1340  
  767         1209  
  767         1248  
159             return 1 if not is_type('number', $data)
160 767 50 66     2357 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161 547 100       2562 return 1 if 0+$data >= $schema->{minimum};
162 266         17852 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
163             }
164              
165 489     489   2137 sub _traverse_keyword_exclusiveMinimum { goto \&_assert_number }
166              
167 453     453   906 sub _eval_keyword_exclusiveMinimum ($self, $data, $schema, $state) {
  453         875  
  453         839  
  453         791  
  453         792  
  453         718  
168             return 1 if not is_type('number', $data)
169 453 50 66     1413 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
170 248 100       1130 return 1 if 0+$data > $schema->{exclusiveMinimum};
171 132         8489 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
172             }
173              
174 642     642   2675 sub _traverse_keyword_maxLength { goto \&_assert_non_negative_integer }
175              
176 585     585   1110 sub _eval_keyword_maxLength ($self, $data, $schema, $state) {
  585         1110  
  585         1075  
  585         1061  
  585         1031  
  585         902  
177 585 100       1677 return 1 if not is_type('string', $data);
178 360 100       1785 return 1 if length($data) <= $schema->{maxLength};
179 170         678 return E($state, 'length is greater than %d', $schema->{maxLength});
180             }
181              
182 603     603   2451 sub _traverse_keyword_minLength { goto \&_assert_non_negative_integer }
183              
184 553     553   996 sub _eval_keyword_minLength ($self, $data, $schema, $state) {
  553         1070  
  553         1021  
  553         890  
  553         933  
  553         871  
185              
186 553 100       1554 return 1 if not is_type('string', $data);
187 327 100       1549 return 1 if length($data) >= $schema->{minLength};
188 154         662 return E($state, 'length is less than %d', $schema->{minLength});
189             }
190              
191 990     990   1788 sub _traverse_keyword_pattern ($self, $schema, $state) {
  990         1888  
  990         1685  
  990         1600  
  990         1589  
192             return if not assert_keyword_type($state, $schema, 'string')
193 990 100 66     2829 or not assert_pattern($state, $schema->{pattern});
194 989         3352 return 1;
195             }
196              
197 1032     1032   2019 sub _eval_keyword_pattern ($self, $data, $schema, $state) {
  1032         2077  
  1032         2039  
  1032         1701  
  1032         1711  
  1032         1573  
198 1032 100       3160 return 1 if not is_type('string', $data);
199              
200 789 100       9107 return 1 if $data =~ m/(?:$schema->{pattern})/;
201 370         1625 return E($state, 'pattern does not match');
202             }
203              
204 512     512   1845 sub _traverse_keyword_maxItems { goto \&_assert_non_negative_integer }
205              
206 441     441   769 sub _eval_keyword_maxItems ($self, $data, $schema, $state) {
  441         824  
  441         787  
  441         737  
  441         674  
  441         678  
207 441 100       1218 return 1 if not is_type('array', $data);
208 264 100       1155 return 1 if @$data <= $schema->{maxItems};
209 126 100       707 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
210             }
211              
212 526     526   1931 sub _traverse_keyword_minItems { goto \&_assert_non_negative_integer }
213              
214 492     492   964 sub _eval_keyword_minItems ($self, $data, $schema, $state) {
  492         989  
  492         922  
  492         843  
  492         815  
  492         813  
215 492 100       1425 return 1 if not is_type('array', $data);
216 274 100       1207 return 1 if @$data >= $schema->{minItems};
217 128 100       663 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
218             }
219              
220 856     856   1637 sub _traverse_keyword_uniqueItems ($self, $schema, $state) {
  856         1527  
  856         1700  
  856         1404  
  856         1395  
221 856 50       2444 return if not assert_keyword_type($state, $schema, 'boolean');
222 856         11751 return 1;
223             }
224              
225 831     831   1537 sub _eval_keyword_uniqueItems ($self, $data, $schema, $state) {
  831         1506  
  831         1505  
  831         1377  
  831         1346  
  831         1329  
226 831 100       2638 return 1 if not is_type('array', $data);
227 619 100       3187 return 1 if not $schema->{uniqueItems};
228 454 100       4840 return 1 if is_elements_unique($data, my $equal_indices = []);
229 205         881 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
230             }
231              
232             # Note: no effort is made to check if the 'contains' keyword has been disabled via its vocabulary.
233 92     92   378 sub _traverse_keyword_maxContains { goto \&_assert_non_negative_integer }
234              
235 84     84   158 sub _eval_keyword_maxContains ($self, $data, $schema, $state) {
  84         157  
  84         139  
  84         144  
  84         126  
  84         151  
236 84 100       246 return 1 if not exists $state->{_num_contains};
237 76 50       266 return 1 if not is_type('array', $data);
238              
239             return E($state, 'contains too many matching items')
240 76 100       342 if $state->{_num_contains} > $schema->{maxContains};
241              
242 44         139 return 1;
243             }
244              
245 124     124   439 sub _traverse_keyword_minContains { goto \&_assert_non_negative_integer }
246              
247 102     102   182 sub _eval_keyword_minContains ($self, $data, $schema, $state) {
  102         182  
  102         173  
  102         160  
  102         156  
  102         172  
248 102 100       280 return 1 if not exists $state->{_num_contains};
249 94 50       260 return 1 if not is_type('array', $data);
250              
251             return E($state, 'contains too few matching items')
252 94 100       390 if $state->{_num_contains} < $schema->{minContains};
253              
254 60         193 return 1;
255             }
256              
257 396     396   2013 sub _traverse_keyword_maxProperties { goto \&_assert_non_negative_integer }
258              
259 356     356   640 sub _eval_keyword_maxProperties ($self, $data, $schema, $state) {
  356         667  
  356         652  
  356         589  
  356         573  
  356         574  
260 356 100       982 return 1 if not is_type('object', $data);
261 210 100       1017 return 1 if keys %$data <= $schema->{maxProperties};
262             return E($state, 'more than %d propert%s', $schema->{maxProperties},
263 102 100       560 $schema->{maxProperties} > 1 ? 'ies' : 'y');
264             }
265              
266 414     414   1436 sub _traverse_keyword_minProperties { goto \&_assert_non_negative_integer }
267              
268 356     356   1059 sub _eval_keyword_minProperties ($self, $data, $schema, $state) {
  356         636  
  356         640  
  356         557  
  356         588  
  356         558  
269 356 100       995 return 1 if not is_type('object', $data);
270 210 100       1098 return 1 if keys %$data >= $schema->{minProperties};
271             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
272 102 100       572 $schema->{minProperties} > 1 ? 'ies' : 'y');
273             }
274              
275 1707     1707   3026 sub _traverse_keyword_required ($self, $schema, $state) {
  1707         3137  
  1707         2943  
  1707         2950  
  1707         2866  
276 1707 50       4512 return if not assert_keyword_type($state, $schema, 'array');
277             return E($state, '"required" element is not a string')
278 1707 50   1905   10344 if any { !is_type('string', $_) } $schema->{required}->@*;
  1905         5105  
279 1707 50       9004 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
280 1707         5086 return 1;
281             }
282              
283 1719     1719   3008 sub _eval_keyword_required ($self, $data, $schema, $state) {
  1719         3590  
  1719         2973  
  1719         2818  
  1719         2870  
  1719         2790  
284 1719 100       4564 return 1 if not is_type('object', $data);
285              
286 1561         6858 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
287 1561 100       5685 return 1 if not @missing;
288 711 100       3644 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
289             }
290              
291 320     320   632 sub _traverse_keyword_dependentRequired ($self, $schema, $state) {
  320         618  
  320         530  
  320         511  
  320         511  
292 320 50       880 return if not assert_keyword_type($state, $schema, 'object');
293              
294 320         655 my $valid = 1;
295 320         1299 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
296             $valid = E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
297 336 50       1021 if not is_type('array', $schema->{dependentRequired}{$property});
298              
299 336         1236 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
300             $valid = E({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
301 350 100       965 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
302             }
303              
304             $valid = E({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
305 336 50       1191 if not is_elements_unique($schema->{dependentRequired}{$property});
306             }
307 320         1016 return $valid;
308             }
309              
310 290     290   573 sub _eval_keyword_dependentRequired ($self, $data, $schema, $state) {
  290         586  
  290         539  
  290         487  
  290         479  
  290         451  
311 290 100       792 return 1 if not is_type('object', $data);
312              
313 185         725 my $valid = 1;
314 185         755 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
315 201 100       609 next if not exists $data->{$property};
316              
317 165 100       958 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
318 85 100       1540 $valid = E({ %$state, _schema_path_suffix => $property },
319             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
320             }
321             }
322              
323 185 100       773 return 1 if $valid;
324 85         306 return E($state, 'not all dependencies are satisfied');
325             }
326              
327 2595     2595   4973 sub _assert_number ($self, $schema, $state) {
  2595         4734  
  2595         4779  
  2595         4198  
  2595         4382  
328 2595 100       8114 return if not assert_keyword_type($state, $schema, 'number');
329 2587         8552 return 1;
330             }
331              
332 3309     3309   6312 sub _assert_non_negative_integer ($self, $schema, $state) {
  3309         5890  
  3309         5370  
  3309         5252  
  3309         5156  
333 3309 50       9310 return if not assert_keyword_type($state, $schema, 'integer');
334             return E($state, '%s value is not a non-negative integer', $state->{keyword})
335 3309 50       11482 if $schema->{$state->{keyword}} < 0;
336 3309         31427 return 1;
337             }
338              
339             1;
340              
341             __END__
342              
343             =pod
344              
345             =encoding UTF-8
346              
347             =head1 NAME
348              
349             JSON::Schema::Modern::Vocabulary::Validation - Implementation of the JSON Schema Validation vocabulary
350              
351             =head1 VERSION
352              
353             version 0.570
354              
355             =head1 DESCRIPTION
356              
357             =for Pod::Coverage vocabulary evaluation_order keywords
358              
359             =for stopwords metaschema
360              
361             Implementation of the JSON Schema Draft 2020-12 "Validation" vocabulary, indicated in metaschemas
362             with the URI C<https://json-schema.org/draft/2020-12/vocab/validation> and formally specified in
363             L<https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6>.
364              
365             Support is also provided for
366              
367             =over 4
368              
369             =item *
370              
371             the equivalent Draft 2019-09 keywords, indicated in metaschemas with the URI C<https://json-schema.org/draft/2019-09/vocab/validation> and formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-6>.
372              
373             =item *
374              
375             the equivalent Draft 7 keywords that correspond to this vocabulary and are formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6>.
376              
377             =back
378              
379             =for stopwords OpenAPI
380              
381             =head1 SUPPORT
382              
383             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
384              
385             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
386              
387             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
388             server|https://open-api.slack.com>, which are also great resources for finding help.
389              
390             =head1 AUTHOR
391              
392             Karen Etheridge <ether@cpan.org>
393              
394             =head1 COPYRIGHT AND LICENCE
395              
396             This software is copyright (c) 2020 by Karen Etheridge.
397              
398             This is free software; you can redistribute it and/or modify it under
399             the same terms as the Perl 5 programming language system itself.
400              
401             =cut