File Coverage

blib/lib/JSON/Schema/Modern/Vocabulary/Validation.pm
Criterion Covered Total %
statement 365 367 99.4
branch 149 170 87.6
condition 80 98 81.6
subroutine 53 53 100.0
pod 0 3 0.0
total 647 691 93.6


line stmt bran cond sub pod time code
1 38     38   1566 use strict;
  38         91  
  38         1672  
2 38     38   206 use warnings;
  38         85  
  38         3359  
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.634';
8              
9 38     38   638 use 5.020;
  38         151  
10 38     38   221 use Moo;
  38         78  
  38         328  
11 38     38   16008 use strictures 2;
  38         312  
  38         1539  
12 38     38   17828 use stable 0.031 'postderef';
  38         714  
  38         279  
13 38     38   7573 use experimental 'signatures';
  38         90  
  38         327  
14 38     38   2715 no autovivification warn => qw(fetch store exists delete);
  38         83  
  38         305  
15 38     38   3148 use if "$]" >= 5.022, experimental => 're_strict';
  38         113  
  38         903  
16 38     38   3692 no if "$]" >= 5.031009, feature => 'indirect';
  38         96  
  38         2840  
17 38     38   270 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         81  
  38         2712  
18 38     38   224 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         150  
  38         2575  
19 38     38   266 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         118  
  38         2179  
20 38     38   226 no feature 'switch';
  38         92  
  38         2163  
21 38     38   204 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         116  
  38         1846  
22 38     38   190 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         112  
  38         674  
23 38     38   3530 use Scalar::Util 'looks_like_number';
  38         93  
  38         3263  
24 38     38   271 use JSON::Schema::Modern::Utilities qw(is_type get_type is_bignum is_equal is_elements_unique E assert_keyword_type assert_pattern jsonp sprintf_num);
  38         114  
  38         4048  
25 38     38   55813 use Math::BigFloat;
  38         3911073  
  38         958  
26 38     38   1291920 use namespace::clean;
  38         104  
  38         1007  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 55 sub vocabulary ($class) {
  22         65  
  22         59  
31 22         138 'https://json-schema.org/draft/2019-09/vocab/validation' => 'draft2019-09',
32             'https://json-schema.org/draft/2020-12/vocab/validation' => 'draft2020-12';
33             }
34              
35 14     14 0 51 sub evaluation_order ($class) { 1 }
  14         32  
  14         28  
  14         76  
36              
37 167     167 0 10101 sub keywords ($class, $spec_version) {
  167         396  
  167         347  
  167         321  
38             return (
39 167 100       15773 qw(type enum),
    100          
    100          
40             $spec_version ne 'draft4' ? 'const' : (),
41             qw(multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
42             maxLength minLength pattern maxItems minItems uniqueItems),
43             $spec_version !~ /^draft[467]\z/ ? qw(maxContains minContains) : (),
44             qw(maxProperties minProperties required),
45             $spec_version !~ /^draft[467]\z/ ? 'dependentRequired' : (),
46             );
47             }
48              
49 8537     8537   18658 sub _traverse_keyword_type ($class, $schema, $state) {
  8537         18199  
  8537         16033  
  8537         14768  
  8537         15431  
50 8537 100       45983 if (ref $schema->{type} eq 'ARRAY') {
51             # Note: this is not actually in the spec, but the restriction exists in the metaschema
52 364 50       1902 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1761 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1771 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     8484  
57             }
58 360 50       2034 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8173 100       33420 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8169 100 50     23925 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38510   50     126396  
64             }
65 8525         32138 return 1;
66             }
67              
68 8942     8942   21862 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8942         19018  
  8942         19868  
  8942         16904  
  8942         24965  
  8942         16134  
69 8942 100       63610 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8942 100       66541 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8942 100       24000 return 1 if any {
73             $type eq $_ or ($_ eq 'number' and $type eq 'integer')
74             or ($type eq 'string' and $state->{stringy_numbers} and looks_like_number($data)
75             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
76 9192 100 100     128508 or ($_ eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR')
      100        
      100        
      100        
      100        
      100        
      66        
      100        
      100        
77             } @want;
78              
79 2191 100       17826 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   2127 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         1911  
  862         1696  
  862         1505  
  862         1838  
83 862         3640 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   1901 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1624  
  770         1470  
  770         1600  
  770         1559  
  770         1417  
87 770         1485 my @s; my $idx = 0;
  770         1378  
88 770         3914 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       2983 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         29069  
90             return E($state, 'value does not match'
91 341 100       4168 .(!(grep $_->{path}, @s) ? ''
92             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
93             }
94              
95 1471     1471   3933 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1471         3825  
  1471         3559  
  1471         3259  
  1471         2888  
  1471         5932  
96              
97 1376     1376   3753 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1376         3960  
  1376         3146  
  1376         3136  
  1376         2945  
  1376         2928  
98 1376         7693 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
99 1376 100       8285 return 1 if is_equal($data, $schema->{const}, \%s);
100 650 100       17282 return E($state, 'value does not match'.($s{path} ? " (at '$s{path}': $s{error})" : ''));
101             }
102              
103 1119     1119   2914 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         3175  
  1119         2830  
  1119         2546  
  1119         2538  
104 1119 100       5364 return if not assert_keyword_type($state, $schema, 'number');
105 1117 50       6456 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
106 1117         73956 return 1;
107             }
108              
109 1126     1126   3240 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         2989  
  1126         2876  
  1126         2567  
  1126         2344  
  1126         2673  
110             return 1 if not is_type('number', $data)
111             and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data)
112 1126 50 66     5581 and do { $data = 0+$data; 1 });
  2   66     12  
  2   33     12  
      100        
113              
114 894         2383 my $remainder;
115              
116 894 100 100     3029 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
117 767         2799 $remainder = $data % $schema->{multipleOf};
118             }
119             else {
120             # if either value is a float, use the bignum library for the calculation for an accurate remainder
121 127 100       2730 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
122 127 100       17429 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
123 127         9670 $remainder = $dividend->bmod($divisor);
124             }
125              
126 894 100       84603 return 1 if $remainder == 0;
127 417         31272 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
128             }
129              
130             *_traverse_keyword_maximum = \&_assert_number;
131              
132 825     825   2818 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         2647  
  825         2153  
  825         1856  
  825         2031  
  825         1782  
133             return 1 if not is_type('number', $data)
134 825 50 66     5202 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
135              
136 582 100       4032 return 1 if 0+$data < $schema->{maximum};
137 415 100 100     54676 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
138 6         124 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
139             }
140             else {
141 409 100       2962 return 1 if 0+$data == $schema->{maximum};
142 246         23634 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
143             }
144             }
145              
146 580     580   1596 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1801  
  580         1450  
  580         1386  
  580         1243  
147 580 100       5095 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
148              
149 18 50       89 return if not assert_keyword_type($state, $schema, 'boolean');
150             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
151 18 50       88 if not exists $schema->{maximum};
152 18         66 return 1;
153             }
154              
155 563     563   1734 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1734  
  563         1509  
  563         1405  
  563         1376  
  563         1193  
156             # we do the work in "maximum" for draft4 so we don't generate multiple errors
157 563 100       3010 return 1 if $state->{specification_version} eq 'draft4';
158              
159             return 1 if not is_type('number', $data)
160 548 50 66     2711 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161              
162 319 100       6671 return 1 if 0+$data < $schema->{exclusiveMaximum};
163 171         25028 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{exclusiveMaximum}));
164             }
165              
166             *_traverse_keyword_minimum = \&_assert_number;
167              
168 946     946   2529 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         2420  
  946         2266  
  946         1950  
  946         1999  
  946         1654  
169             return 1 if not is_type('number', $data)
170 946 50 66     5369 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
171              
172 687 100       4093 return 1 if 0+$data > $schema->{minimum};
173 511 100 100     81626 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
174 4         80 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
175             }
176             else {
177 507 100       2942 return 1 if 0+$data == $schema->{minimum};
178 323         46638 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
179             }
180             }
181              
182 515     515   1422 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         1631  
  515         1308  
  515         1294  
  515         1209  
183 515 100       4501 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
184              
185 16 50       57 return if not assert_keyword_type($state, $schema, 'boolean');
186             return E($state, 'use of exclusiveMinimum requires the presence of minimum')
187 16 50       72 if not exists $schema->{minimum};
188 16         58 return 1;
189             }
190              
191 496     496   1513 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         1462  
  496         1378  
  496         1948  
  496         1160  
  496         1125  
192             # we do the work in "minimum" for draft4 so we don't generate multiple errors
193 496 100       2678 return 1 if $state->{specification_version} eq 'draft4';
194              
195             return 1 if not is_type('number', $data)
196 485 50 66     2279 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
197              
198 256 100       2141 return 1 if 0+$data > $schema->{exclusiveMinimum};
199 138         23041 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{exclusiveMinimum}));
200             }
201              
202             *_traverse_keyword_maxLength = \&_assert_non_negative_integer;
203              
204 666     666   1983 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1961  
  666         2002  
  666         1768  
  666         1719  
  666         1497  
205 666 100       3194 return 1 if not is_type('string', $data);
206 417 100       3264 return 1 if length($data) <= $schema->{maxLength};
207 193         1233 return E($state, 'length is greater than %d', $schema->{maxLength});
208             }
209              
210             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
211              
212 621     621   1977 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1956  
  621         1888  
  621         1758  
  621         1536  
  621         1429  
213 621 100       3065 return 1 if not is_type('string', $data);
214 368 100       2973 return 1 if length($data) >= $schema->{minLength};
215 169         1202 return E($state, 'length is less than %d', $schema->{minLength});
216             }
217              
218 1288     1288   3635 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         3739  
  1288         2738  
  1288         2934  
  1288         2878  
219             return if not assert_keyword_type($state, $schema, 'string')
220 1288 100 66     6289 or not assert_pattern($state, $schema->{pattern});
221 1287         6230 return 1;
222             }
223              
224 1278     1278   3589 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         3762  
  1278         3524  
  1278         3404  
  1278         2975  
  1278         2499  
225 1278 100       5874 return 1 if not is_type('string', $data);
226              
227 989 100       25416 return 1 if $data =~ m/(?:$schema->{pattern})/;
228 452         2547 return E($state, 'pattern does not match');
229             }
230              
231             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
232              
233 488     488   1513 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         1469  
  488         1283  
  488         1189  
  488         1228  
  488         1066  
234 488 100       2556 return 1 if not is_type('array', $data);
235 292 100       1959 return 1 if @$data <= $schema->{maxItems};
236 136 100       1204 return E($state, 'array has more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
237             }
238              
239             *_traverse_keyword_minItems = \&_assert_non_negative_integer;
240              
241 531     531   1454 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         1635  
  531         1376  
  531         1181  
  531         1232  
  531         1048  
242 531 100       2323 return 1 if not is_type('array', $data);
243 309 100       2270 return 1 if @$data >= $schema->{minItems};
244 142 100       1207 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
245             }
246              
247 1149     1149   3313 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         3140  
  1149         2502  
  1149         2369  
  1149         2590  
248 1149         5992 return assert_keyword_type($state, $schema, 'boolean');
249             }
250              
251 1120     1120   2842 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         3172  
  1120         2601  
  1120         2653  
  1120         2412  
  1120         2488  
252 1120 100       5339 return 1 if not is_type('array', $data);
253 908 100       8149 return 1 if not $schema->{uniqueItems};
254 628 100       12313 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
255 280         1865 return E($state, 'items at indices %d and %d are not unique', $s->{equal_indices}->@*);
256             }
257              
258             # The evaluation implementations of maxContains and minContains are in the Applicator vocabulary,
259             # as 'contains' needs to run first
260             *_traverse_keyword_maxContains = \&_assert_non_negative_integer;
261              
262             *_traverse_keyword_minContains = \&_assert_non_negative_integer;
263              
264             *_traverse_keyword_maxProperties = \&_assert_non_negative_integer;
265              
266 399     399   1097 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         1406  
  399         1090  
  399         1075  
  399         984  
  399         975  
267 399 100       1799 return 1 if not is_type('object', $data);
268 234 100       1976 return 1 if keys %$data <= $schema->{maxProperties};
269             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
270 112 100       972 $schema->{maxProperties} > 1 ? 'ies' : 'y');
271             }
272              
273             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
274              
275 403     403   1203 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         1091  
  403         1107  
  403         940  
  403         888  
  403         833  
276 403 100       1880 return 1 if not is_type('object', $data);
277 238 100       1931 return 1 if keys %$data >= $schema->{minProperties};
278             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
279 116 100       1028 $schema->{minProperties} > 1 ? 'ies' : 'y');
280             }
281              
282 2107     2107   4952 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         4760  
  2107         4121  
  2107         4311  
  2107         3639  
283 2107 50       8020 return if not assert_keyword_type($state, $schema, 'array');
284 2107 50 66     11762 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
285              
286 2107 50       14651 if (my @non_string = grep !is_type('string', $schema->{required}->[$_]), 0 .. $schema->{required}->$#*) {
287             ()= E({ %$state, _keyword_path_suffix => $_ }, '"required" element is not a string')
288 0         0 foreach @non_string;
289 0         0 return;
290             }
291              
292 2107 50       10287 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
293 2107         8498 return 1;
294             }
295              
296 2273     2273   5803 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         5519  
  2273         5198  
  2273         4526  
  2273         5001  
  2273         4249  
297 2273 100       10521 return 1 if not is_type('object', $data);
298              
299 2088         14039 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
300 2088 100       9534 return 1 if not @missing;
301 823 100       7021 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
302             }
303              
304 319     319   829 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         1054  
  319         792  
  319         673  
  319         636  
305 319 50       1467 return if not assert_keyword_type($state, $schema, 'object');
306              
307 319         890 my $valid = 1;
308 319         2129 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
309             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
310 335 50       1470 if not is_type('array', $schema->{dependentRequired}{$property});
311              
312 335         1834 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
313             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
314 349 100       1574 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
315             }
316              
317             $valid = E({ %$state, _keyword_path_suffix => $property }, 'elements are not unique')
318 335 50       2075 if not is_elements_unique($schema->{dependentRequired}{$property});
319             }
320 319         1508 return $valid;
321             }
322              
323 295     295   805 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         846  
  295         691  
  295         649  
  295         660  
  295         627  
324 295 100       1307 return 1 if not is_type('object', $data);
325              
326 185         616 my $valid = 1;
327 185         1277 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
328 201 100       1025 next if not exists $data->{$property};
329              
330 165 100       1528 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
331 85 100       2023 $valid = E({ %$state, _keyword_path_suffix => $property },
332             'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
333             }
334             }
335              
336 185 100       992 return 1 if $valid;
337 85         348 return E($state, 'not all dependencies are satisfied');
338             }
339              
340 1827     1827   5435 sub _assert_number ($class, $schema, $state) {
  1827         5139  
  1827         4469  
  1827         4058  
  1827         4084  
341 1827         9604 return assert_keyword_type($state, $schema, 'number');
342             }
343              
344 3683     3683   10011 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         10587  
  3683         9194  
  3683         8441  
  3683         8629  
345 3683 50       18053 return if not assert_keyword_type($state, $schema, 'integer');
346             return E($state, '%s value is not a non-negative integer', $state->{keyword})
347 3683 50       24849 if $schema->{$state->{keyword}} < 0;
348 3683         85013 return 1;
349             }
350              
351             1;
352              
353             __END__