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   1176 use strict;
  38         66  
  38         1296  
2 38     38   143 use warnings;
  38         67  
  38         2671  
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.635';
8              
9 38     38   531 use 5.020;
  38         110  
10 38     38   153 use Moo;
  38         77  
  38         226  
11 38     38   10870 use strictures 2;
  38         237  
  38         1145  
12 38     38   12759 use stable 0.031 'postderef';
  38         491  
  38         267  
13 38     38   5660 use experimental 'signatures';
  38         90  
  38         130  
14 38     38   1823 no autovivification warn => qw(fetch store exists delete);
  38         67  
  38         254  
15 38     38   2384 use if "$]" >= 5.022, experimental => 're_strict';
  38         113  
  38         686  
16 38     38   2547 no if "$]" >= 5.031009, feature => 'indirect';
  38         76  
  38         2126  
17 38     38   164 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         66  
  38         2281  
18 38     38   176 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         65  
  38         1843  
19 38     38   154 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         87  
  38         1275  
20 38     38   140 no feature 'switch';
  38         75  
  38         1451  
21 38     38   138 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         85  
  38         1293  
22 38     38   156 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         65  
  38         705  
23 38     38   2284 use Scalar::Util 'looks_like_number';
  38         96  
  38         3375  
24 38     38   191 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         61  
  38         3132  
25 38     38   47393 use Math::BigFloat;
  38         2599381  
  38         352  
26 38     38   850156 use namespace::clean;
  38         80  
  38         492  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 42 sub vocabulary ($class) {
  22         37  
  22         31  
31 22         121 '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 22 sub evaluation_order ($class) { 1 }
  14         25  
  14         15  
  14         49  
36              
37 167     167 0 1205 sub keywords ($class, $spec_version) {
  167         278  
  167         277  
  167         218  
38             return (
39 167 100       9630 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 8553     8553   11929 sub _traverse_keyword_type ($class, $schema, $state) {
  8553         11427  
  8553         11298  
  8553         10628  
  8553         10463  
50 8553 100       23689 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       1181 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1036 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1203 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     5546  
57             }
58 360 50       1283 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8189 100       21102 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8185 100 50     15071 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38588   50     82194  
64             }
65 8541         20270 return 1;
66             }
67              
68 8951     8951   19109 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8951         12497  
  8951         13262  
  8951         11613  
  8951         10842  
  8951         10707  
69 8951 100       39431 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8951 100       39118 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8951 100       14978 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 9201 100 100     85307 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       11020 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   1440 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         1194  
  862         1179  
  862         1136  
  862         1055  
83 862         2340 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   4441 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1210  
  770         1113  
  770         967  
  770         943  
  770         928  
87 770         1054 my @s; my $idx = 0;
  770         959  
88 770         2504 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       1913 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         19735  
90             return E($state, 'value does not match'
91 341 100       2522 .(!(grep $_->{path}, @s) ? ''
92             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
93             }
94              
95 1473     1473   2522 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1473         2475  
  1473         2368  
  1473         2144  
  1473         2071  
  1473         3663  
96              
97 1379     1379   2445 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1379         2383  
  1379         2418  
  1379         1946  
  1379         1848  
  1379         1797  
98 1379         4735 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
99 1379 100       5653 return 1 if is_equal($data, $schema->{const}, \%s);
100 651 100       9675 return E($state, 'value does not match'.($s{path} ? " (at '$s{path}': $s{error})" : ''));
101             }
102              
103 1119     1119   1877 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         1687  
  1119         1760  
  1119         1611  
  1119         1429  
104 1119 100       3131 return if not assert_keyword_type($state, $schema, 'number');
105 1117 50       3286 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
106 1117         41600 return 1;
107             }
108              
109 1126     1126   1921 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         1814  
  1126         1778  
  1126         1552  
  1126         1463  
  1126         1484  
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     3249 and do { $data = 0+$data; 1 });
  2   66     8  
  2   33     6  
      100        
113              
114 894         1610 my $remainder;
115              
116 894 100 100     1902 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
117 767         1795 $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       1586 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
122 127 100       10430 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
123 127         5575 $remainder = $dividend->bmod($divisor);
124             }
125              
126 894 100       53783 return 1 if $remainder == 0;
127 417         19578 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   1477 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         1359  
  825         1313  
  825         1197  
  825         1231  
  825         1161  
133             return 1 if not is_type('number', $data)
134 825 50 66     2402 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
135              
136 582 100       2306 return 1 if 0+$data < $schema->{maximum};
137 415 100 100     32987 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
138 6         76 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
139             }
140             else {
141 409 100       1442 return 1 if 0+$data == $schema->{maximum};
142 246         14280 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
143             }
144             }
145              
146 580     580   995 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1111  
  580         917  
  580         828  
  580         846  
147 580 100       2769 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
148              
149 18 50       38 return if not assert_keyword_type($state, $schema, 'boolean');
150             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
151 18 50       50 if not exists $schema->{maximum};
152 18         35 return 1;
153             }
154              
155 563     563   1010 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1005  
  563         864  
  563         940  
  563         821  
  563         735  
156             # we do the work in "maximum" for draft4 so we don't generate multiple errors
157 563 100       1576 return 1 if $state->{specification_version} eq 'draft4';
158              
159             return 1 if not is_type('number', $data)
160 548 50 66     1495 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161              
162 319 100       1423 return 1 if 0+$data < $schema->{exclusiveMaximum};
163 171         15262 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   1563 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         1490  
  946         1346  
  946         1278  
  946         1294  
  946         1133  
169             return 1 if not is_type('number', $data)
170 946 50 66     2556 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
171              
172 687 100       2450 return 1 if 0+$data > $schema->{minimum};
173 511 100 100     49384 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
174 4         54 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
175             }
176             else {
177 507 100       1616 return 1 if 0+$data == $schema->{minimum};
178 323         28666 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
179             }
180             }
181              
182 515     515   959 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         922  
  515         794  
  515         746  
  515         694  
183 515 100       2422 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
184              
185 16 50       31 return if not assert_keyword_type($state, $schema, 'boolean');
186             return E($state, 'use of exclusiveMinimum requires the presence of minimum')
187 16 50       47 if not exists $schema->{minimum};
188 16         34 return 1;
189             }
190              
191 496     496   924 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         855  
  496         962  
  496         741  
  496         731  
  496         706  
192             # we do the work in "minimum" for draft4 so we don't generate multiple errors
193 496 100       1411 return 1 if $state->{specification_version} eq 'draft4';
194              
195             return 1 if not is_type('number', $data)
196 485 50 66     1443 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
197              
198 256 100       1154 return 1 if 0+$data > $schema->{exclusiveMinimum};
199 138         14371 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   1217 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1213  
  666         1112  
  666         1093  
  666         983  
  666         862  
205 666 100       1908 return 1 if not is_type('string', $data);
206 417 100       1819 return 1 if length($data) <= $schema->{maxLength};
207 193         818 return E($state, 'length is greater than %d', $schema->{maxLength});
208             }
209              
210             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
211              
212 621     621   1090 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1076  
  621         1161  
  621         971  
  621         882  
  621         840  
213 621 100       1703 return 1 if not is_type('string', $data);
214 368 100       1596 return 1 if length($data) >= $schema->{minLength};
215 169         707 return E($state, 'length is less than %d', $schema->{minLength});
216             }
217              
218 1288     1288   2898 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         2286  
  1288         2016  
  1288         1895  
  1288         2081  
219             return if not assert_keyword_type($state, $schema, 'string')
220 1288 100 66     3648 or not assert_pattern($state, $schema->{pattern});
221 1287         3953 return 1;
222             }
223              
224 1278     1278   2398 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         2455  
  1278         2169  
  1278         1973  
  1278         1903  
  1278         2072  
225 1278 100       3612 return 1 if not is_type('string', $data);
226              
227 989 100       13072 return 1 if $data =~ m/(?:$schema->{pattern})/;
228 452         1591 return E($state, 'pattern does not match');
229             }
230              
231             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
232              
233 488     488   836 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         868  
  488         731  
  488         698  
  488         662  
  488         619  
234 488 100       1318 return 1 if not is_type('array', $data);
235 292 100       1152 return 1 if @$data <= $schema->{maxItems};
236 136 100       709 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   945 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         904  
  531         809  
  531         750  
  531         800  
  531         674  
242 531 100       1386 return 1 if not is_type('array', $data);
243 309 100       1282 return 1 if @$data >= $schema->{minItems};
244 142 100       821 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
245             }
246              
247 1149     1149   1851 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         1808  
  1149         1555  
  1149         1451  
  1149         1398  
248 1149         5453 return assert_keyword_type($state, $schema, 'boolean');
249             }
250              
251 1120     1120   1873 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         1811  
  1120         1684  
  1120         1629  
  1120         1488  
  1120         1403  
252 1120 100       2968 return 1 if not is_type('array', $data);
253 908 100       4238 return 1 if not $schema->{uniqueItems};
254 628 100       7193 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
255 280         1101 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   701 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         699  
  399         710  
  399         954  
  399         559  
  399         521  
267 399 100       896 return 1 if not is_type('object', $data);
268 234 100       1061 return 1 if keys %$data <= $schema->{maxProperties};
269             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
270 112 100       589 $schema->{maxProperties} > 1 ? 'ies' : 'y');
271             }
272              
273             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
274              
275 403     403   714 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         666  
  403         632  
  403         579  
  403         635  
  403         557  
276 403 100       977 return 1 if not is_type('object', $data);
277 238 100       1097 return 1 if keys %$data >= $schema->{minProperties};
278             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
279 116 100       1060 $schema->{minProperties} > 1 ? 'ies' : 'y');
280             }
281              
282 2107     2107   3551 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         3507  
  2107         2990  
  2107         2646  
  2107         2478  
283 2107 50       5404 return if not assert_keyword_type($state, $schema, 'array');
284 2107 50 66     6470 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
285              
286 2107 50       8413 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       6303 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
293 2107         5312 return 1;
294             }
295              
296 2273     2273   4122 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         3672  
  2273         3563  
  2273         2839  
  2273         3250  
  2273         2851  
297 2273 100       6259 return 1 if not is_type('object', $data);
298              
299 2088         8878 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
300 2088 100       6461 return 1 if not @missing;
301 823 100       4383 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
302             }
303              
304 319     319   568 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         554  
  319         483  
  319         512  
  319         479  
305 319 50       967 return if not assert_keyword_type($state, $schema, 'object');
306              
307 319         567 my $valid = 1;
308 319         1245 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
309             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
310 335 50       963 if not is_type('array', $schema->{dependentRequired}{$property});
311              
312 335         1232 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
313             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
314 349 100       909 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       1268 if not is_elements_unique($schema->{dependentRequired}{$property});
319             }
320 319         933 return $valid;
321             }
322              
323 295     295   529 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         525  
  295         452  
  295         464  
  295         453  
  295         409  
324 295 100       775 return 1 if not is_type('object', $data);
325              
326 185         304 my $valid = 1;
327 185         808 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
328 201 100       567 next if not exists $data->{$property};
329              
330 165 100       879 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
331 85 100       1300 $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       656 return 1 if $valid;
337 85         235 return E($state, 'not all dependencies are satisfied');
338             }
339              
340 1827     1827   3520 sub _assert_number ($class, $schema, $state) {
  1827         3201  
  1827         2984  
  1827         2570  
  1827         2562  
341 1827         5173 return assert_keyword_type($state, $schema, 'number');
342             }
343              
344 3683     3683   6697 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         6456  
  3683         5576  
  3683         5763  
  3683         5365  
345 3683 50       9859 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       13634 if $schema->{$state->{keyword}} < 0;
348 3683         49155 return 1;
349             }
350              
351             1;
352              
353             __END__