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   1172 use strict;
  38         85  
  38         1484  
2 38     38   163 use warnings;
  38         61  
  38         2783  
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.637';
8              
9 38     38   557 use 5.020;
  38         120  
10 38     38   172 use Moo;
  38         65  
  38         303  
11 38     38   12262 use strictures 2;
  38         234  
  38         1192  
12 38     38   13668 use stable 0.031 'postderef';
  38         549  
  38         194  
13 38     38   5882 use experimental 'signatures';
  38         93  
  38         132  
14 38     38   1952 no autovivification warn => qw(fetch store exists delete);
  38         99  
  38         313  
15 38     38   2550 use if "$]" >= 5.022, experimental => 're_strict';
  38         85  
  38         745  
16 38     38   2790 no if "$]" >= 5.031009, feature => 'indirect';
  38         64  
  38         2338  
17 38     38   192 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         71  
  38         1926  
18 38     38   172 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         97  
  38         1782  
19 38     38   153 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         65  
  38         1313  
20 38     38   155 no feature 'switch';
  38         77  
  38         1379  
21 38     38   140 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         87  
  38         1416  
22 38     38   167 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         69  
  38         658  
23 38     38   2423 use Scalar::Util 'looks_like_number';
  38         86  
  38         2849  
24 38     38   233 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         77  
  38         3639  
25 38     38   61003 use Math::BigFloat;
  38         2682377  
  38         230  
26 38     38   877495 use namespace::clean;
  38         86  
  38         508  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 41 sub vocabulary ($class) {
  22         40  
  22         38  
31 22         92 '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 25 sub evaluation_order ($class) { 1 }
  14         28  
  14         22  
  14         52  
36              
37 167     167 0 1201 sub keywords ($class, $spec_version) {
  167         284  
  167         258  
  167         289  
38             return (
39 167 100       10368 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   13197 sub _traverse_keyword_type ($class, $schema, $state) {
  8553         14936  
  8553         12642  
  8553         11139  
  8553         10890  
50 8553 100       25713 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       1388 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1227 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1317 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     6253  
57             }
58 360 50       1512 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8189 100       23831 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8185 100 50     16789 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38588   50     88590  
64             }
65 8541         20567 return 1;
66             }
67              
68 8951     8951   15516 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8951         13344  
  8951         13049  
  8951         12025  
  8951         12311  
  8951         11150  
69 8951 100       45484 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8951 100       45050 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8951 100       15597 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     85046 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       12238 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   1558 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         1342  
  862         1223  
  862         1157  
  862         1115  
83 862         2500 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   4991 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1195  
  770         1055  
  770         1084  
  770         1168  
  770         954  
87 770         1015 my @s; my $idx = 0;
  770         1089  
88 770         2910 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       2060 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         19706  
90             return E($state, 'value does not match'
91 341 100       2897 .(!(grep $_->{path}, @s) ? ''
92             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
93             }
94              
95 1473     1473   2700 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1473         2603  
  1473         2580  
  1473         2365  
  1473         2144  
  1473         3622  
96              
97 1379     1379   2596 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1379         2470  
  1379         2561  
  1379         2238  
  1379         2046  
  1379         2111  
98 1379         5116 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
99 1379 100       6204 return 1 if is_equal($data, $schema->{const}, \%s);
100 651 100       10605 return E($state, 'value does not match'.($s{path} ? " (at '$s{path}': $s{error})" : ''));
101             }
102              
103 1119     1119   1974 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         2037  
  1119         1767  
  1119         1776  
  1119         1655  
104 1119 100       3393 return if not assert_keyword_type($state, $schema, 'number');
105 1117 50       3816 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
106 1117         44514 return 1;
107             }
108              
109 1126     1126   2001 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         2061  
  1126         1783  
  1126         1672  
  1126         1715  
  1126         1488  
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     3237 and do { $data = 0+$data; 1 });
  2   66     8  
  2   33     6  
      100        
113              
114 894         1455 my $remainder;
115              
116 894 100 100     1964 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
117 767         1919 $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       1626 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
122 127 100       11771 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
123 127         6241 $remainder = $dividend->bmod($divisor);
124             }
125              
126 894 100       57163 return 1 if $remainder == 0;
127 417         21462 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   1405 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         1435  
  825         1398  
  825         1407  
  825         1277  
  825         1170  
133             return 1 if not is_type('number', $data)
134 825 50 66     2522 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
135              
136 582 100       2562 return 1 if 0+$data < $schema->{maximum};
137 415 100 100     34756 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
138 6         72 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
139             }
140             else {
141 409 100       1587 return 1 if 0+$data == $schema->{maximum};
142 246         17881 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
143             }
144             }
145              
146 580     580   1015 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1036  
  580         1022  
  580         904  
  580         926  
147 580 100       2941 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
148              
149 18 50       49 return if not assert_keyword_type($state, $schema, 'boolean');
150             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
151 18 50       54 if not exists $schema->{maximum};
152 18         42 return 1;
153             }
154              
155 563     563   1069 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1124  
  563         883  
  563         878  
  563         837  
  563         774  
156             # we do the work in "maximum" for draft4 so we don't generate multiple errors
157 563 100       1762 return 1 if $state->{specification_version} eq 'draft4';
158              
159             return 1 if not is_type('number', $data)
160 548 50 66     1587 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161              
162 319 100       1560 return 1 if 0+$data < $schema->{exclusiveMaximum};
163 171         20435 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   1648 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         1690  
  946         1647  
  946         1489  
  946         1416  
  946         1231  
169             return 1 if not is_type('number', $data)
170 946 50 66     2866 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
171              
172 687 100       2907 return 1 if 0+$data > $schema->{minimum};
173 511 100 100     55152 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
174 4         55 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
175             }
176             else {
177 507 100       1767 return 1 if 0+$data == $schema->{minimum};
178 323         30113 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
179             }
180             }
181              
182 515     515   958 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         974  
  515         797  
  515         828  
  515         724  
183 515 100       2410 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
184              
185 16 50       34 return if not assert_keyword_type($state, $schema, 'boolean');
186             return E($state, 'use of exclusiveMinimum requires the presence of minimum')
187 16 50       45 if not exists $schema->{minimum};
188 16         33 return 1;
189             }
190              
191 496     496   897 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         9915  
  496         843  
  496         782  
  496         888  
  496         720  
192             # we do the work in "minimum" for draft4 so we don't generate multiple errors
193 496 100       1496 return 1 if $state->{specification_version} eq 'draft4';
194              
195             return 1 if not is_type('number', $data)
196 485 50 66     1496 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
197              
198 256 100       1218 return 1 if 0+$data > $schema->{exclusiveMinimum};
199 138         18546 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   1323 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1356  
  666         1202  
  666         1074  
  666         1043  
  666         905  
205 666 100       2114 return 1 if not is_type('string', $data);
206 417 100       1840 return 1 if length($data) <= $schema->{maxLength};
207 193         789 return E($state, 'length is greater than %d', $schema->{maxLength});
208             }
209              
210             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
211              
212 621     621   1253 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1159  
  621         1158  
  621         1032  
  621         925  
  621         893  
213 621 100       1861 return 1 if not is_type('string', $data);
214 368 100       1834 return 1 if length($data) >= $schema->{minLength};
215 169         697 return E($state, 'length is less than %d', $schema->{minLength});
216             }
217              
218 1288     1288   4608 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         3013  
  1288         2305  
  1288         2187  
  1288         2087  
219             return if not assert_keyword_type($state, $schema, 'string')
220 1288 100 66     4108 or not assert_pattern($state, $schema->{pattern});
221 1287         4171 return 1;
222             }
223              
224 1278     1278   2765 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         2687  
  1278         2686  
  1278         2277  
  1278         2268  
  1278         2229  
225 1278 100       4055 return 1 if not is_type('string', $data);
226              
227 989 100       15141 return 1 if $data =~ m/(?:$schema->{pattern})/;
228 452         1841 return E($state, 'pattern does not match');
229             }
230              
231             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
232              
233 488     488   1041 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         996  
  488         916  
  488         953  
  488         837  
  488         750  
234 488 100       1531 return 1 if not is_type('array', $data);
235 292 100       1429 return 1 if @$data <= $schema->{maxItems};
236 136 100       847 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   1039 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         1045  
  531         1040  
  531         881  
  531         788  
  531         773  
242 531 100       1525 return 1 if not is_type('array', $data);
243 309 100       1407 return 1 if @$data >= $schema->{minItems};
244 142 100       826 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
245             }
246              
247 1149     1149   2320 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         2125  
  1149         2083  
  1149         1795  
  1149         1757  
248 1149         4162 return assert_keyword_type($state, $schema, 'boolean');
249             }
250              
251 1120     1120   2283 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         2190  
  1120         2152  
  1120         1899  
  1120         1808  
  1120         1816  
252 1120 100       3826 return 1 if not is_type('array', $data);
253 908 100       5886 return 1 if not $schema->{uniqueItems};
254 628 100       8130 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
255 280         1261 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   794 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         806  
  399         741  
  399         635  
  399         710  
  399         604  
267 399 100       1445 return 1 if not is_type('object', $data);
268 234 100       1208 return 1 if keys %$data <= $schema->{maxProperties};
269             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
270 112 100       744 $schema->{maxProperties} > 1 ? 'ies' : 'y');
271             }
272              
273             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
274              
275 403     403   849 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         727  
  403         721  
  403         684  
  403         696  
  403         724  
276 403 100       1127 return 1 if not is_type('object', $data);
277 238 100       1184 return 1 if keys %$data >= $schema->{minProperties};
278             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
279 116 100       1100 $schema->{minProperties} > 1 ? 'ies' : 'y');
280             }
281              
282 2107     2107   3395 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         3234  
  2107         2935  
  2107         2849  
  2107         2644  
283 2107 50       5351 return if not assert_keyword_type($state, $schema, 'array');
284 2107 50 66     7564 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
285              
286 2107 50       9666 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       7024 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
293 2107         5452 return 1;
294             }
295              
296 2273     2273   4652 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         4235  
  2273         3597  
  2273         3720  
  2273         3337  
  2273         2974  
297 2273 100       7052 return 1 if not is_type('object', $data);
298              
299 2088         10107 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
300 2088 100       6637 return 1 if not @missing;
301 823 100       4669 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
302             }
303              
304 319     319   623 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         755  
  319         533  
  319         510  
  319         610  
305 319 50       970 return if not assert_keyword_type($state, $schema, 'object');
306              
307 319         649 my $valid = 1;
308 319         8993 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
309             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
310 335 50       1024 if not is_type('array', $schema->{dependentRequired}{$property});
311              
312 335         1248 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
313             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
314 349 100       1056 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       1279 if not is_elements_unique($schema->{dependentRequired}{$property});
319             }
320 319         1024 return $valid;
321             }
322              
323 295     295   553 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         588  
  295         584  
  295         505  
  295         523  
  295         460  
324 295 100       846 return 1 if not is_type('object', $data);
325              
326 185         383 my $valid = 1;
327 185         871 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
328 201 100       706 next if not exists $data->{$property};
329              
330 165 100       1106 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
331 85 100       1419 $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       679 return 1 if $valid;
337 85         218 return E($state, 'not all dependencies are satisfied');
338             }
339              
340 1827     1827   3496 sub _assert_number ($class, $schema, $state) {
  1827         3269  
  1827         3102  
  1827         2916  
  1827         2653  
341 1827         5893 return assert_keyword_type($state, $schema, 'number');
342             }
343              
344 3683     3683   7212 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         6734  
  3683         6287  
  3683         5890  
  3683         5637  
345 3683 50       11092 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       14431 if $schema->{$state->{keyword}} < 0;
348 3683         50705 return 1;
349             }
350              
351             1;
352              
353             __END__