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   1468 use strict;
  38         96  
  38         1689  
2 38     38   206 use warnings;
  38         88  
  38         3443  
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.632';
8              
9 38     38   706 use 5.020;
  38         145  
10 38     38   212 use Moo;
  38         78  
  38         296  
11 38     38   16270 use strictures 2;
  38         406  
  38         1662  
12 38     38   18050 use stable 0.031 'postderef';
  38         777  
  38         319  
13 38     38   7675 use experimental 'signatures';
  38         123  
  38         204  
14 38     38   2580 no autovivification warn => qw(fetch store exists delete);
  38         86  
  38         311  
15 38     38   3355 use if "$]" >= 5.022, experimental => 're_strict';
  38         97  
  38         992  
16 38     38   3684 no if "$]" >= 5.031009, feature => 'indirect';
  38         117  
  38         2896  
17 38     38   252 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         92  
  38         2900  
18 38     38   236 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         106  
  38         2857  
19 38     38   239 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         77  
  38         1912  
20 38     38   259 no feature 'switch';
  38         93  
  38         2036  
21 38     38   199 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         100  
  38         1996  
22 38     38   206 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         107  
  38         595  
23 38     38   3224 use Scalar::Util 'looks_like_number';
  38         98  
  38         3348  
24 38     38   240 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         113  
  38         4122  
25 38     38   53267 use Math::BigFloat;
  38         3893165  
  38         1422  
26 38     38   1266759 use namespace::clean;
  38         124  
  38         770  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 81 sub vocabulary ($class) {
  22         68  
  22         71  
31 22         184 '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 49 sub evaluation_order ($class) { 1 }
  14         35  
  14         29  
  14         78  
36              
37 167     167 0 5515 sub keywords ($class, $spec_version) {
  167         365  
  167         310  
  167         293  
38             return (
39 167 100       14770 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   19080 sub _traverse_keyword_type ($class, $schema, $state) {
  8537         17725  
  8537         16502  
  8537         16019  
  8537         18434  
50 8537 100       37229 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       2143 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1751 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1844 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     9987  
57             }
58 360 50       2268 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8173 100       34026 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8169 100 50     23804 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38510   50     127702  
64             }
65 8525         33287 return 1;
66             }
67              
68 8942     8942   20896 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8942         19439  
  8942         19863  
  8942         17325  
  8942         17525  
  8942         15372  
69 8942 100       66913 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8942 100       63291 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8942 100       28262 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     126117 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       19251 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   2095 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         2047  
  862         1717  
  862         1603  
  862         1600  
83 862         3975 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   1993 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1791  
  770         1696  
  770         1614  
  770         1466  
  770         1396  
87 770         1510 my @s; my $idx = 0;
  770         1641  
88 770         4473 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       3242 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         34935  
90             return E($state, 'value does not match'
91 341 100       4292 .(!(grep $_->{path}, @s) ? ''
92             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
93             }
94              
95 1471     1471   3856 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1471         3795  
  1471         3479  
  1471         3194  
  1471         3104  
  1471         6068  
96              
97 1376     1376   3765 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1376         3661  
  1376         3372  
  1376         3042  
  1376         2803  
  1376         2495  
98 1376         7817 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
99 1376 100       8872 return 1 if is_equal($data, $schema->{const}, \%s);
100 650 100       16388 return E($state, 'value does not match'.($s{path} ? " (at '$s{path}': $s{error})" : ''));
101             }
102              
103 1119     1119   2909 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         2833  
  1119         2682  
  1119         2416  
  1119         2239  
104 1119 100       5396 return if not assert_keyword_type($state, $schema, 'number');
105 1117 50       5956 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
106 1117         59080 return 1;
107             }
108              
109 1126     1126   2812 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         2847  
  1126         2711  
  1126         2683  
  1126         2476  
  1126         2399  
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     5483 and do { $data = 0+$data; 1 });
  2   66     10  
  2   33     7  
      100        
113              
114 894         2298 my $remainder;
115              
116 894 100 100     3199 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
117 767         2654 $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       2703 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
122 127 100       14562 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
123 127         8163 $remainder = $dividend->bmod($divisor);
124             }
125              
126 894 100       77761 return 1 if $remainder == 0;
127 417         30280 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   2272 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         2392  
  825         2068  
  825         2003  
  825         1934  
  825         1534  
133             return 1 if not is_type('number', $data)
134 825 50 66     4031 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
135              
136 582 100       3949 return 1 if 0+$data < $schema->{maximum};
137 415 100 100     51292 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
138 6         116 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
139             }
140             else {
141 409 100       2447 return 1 if 0+$data == $schema->{maximum};
142 246         21258 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
143             }
144             }
145              
146 580     580   1631 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1580  
  580         1506  
  580         1326  
  580         1397  
147 580 100       5029 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
148              
149 18 50       60 return if not assert_keyword_type($state, $schema, 'boolean');
150             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
151 18 50       79 if not exists $schema->{maximum};
152 18         54 return 1;
153             }
154              
155 563     563   1641 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1689  
  563         1464  
  563         1392  
  563         1292  
  563         1171  
156             # we do the work in "maximum" for draft4 so we don't generate multiple errors
157 563 100       2860 return 1 if $state->{specification_version} eq 'draft4';
158              
159             return 1 if not is_type('number', $data)
160 548 50 66     2675 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161              
162 319 100       3195 return 1 if 0+$data < $schema->{exclusiveMaximum};
163 171         28346 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   2555 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         2309  
  946         2364  
  946         2328  
  946         1989  
  946         1849  
169             return 1 if not is_type('number', $data)
170 946 50 66     4560 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
171              
172 687 100       4303 return 1 if 0+$data > $schema->{minimum};
173 511 100 100     80868 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
174 4         90 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
175             }
176             else {
177 507 100       3031 return 1 if 0+$data == $schema->{minimum};
178 323         45821 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
179             }
180             }
181              
182 515     515   1616 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         1372  
  515         1129  
  515         1212  
  515         1097  
183 515 100       4474 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
184              
185 16 50       66 return if not assert_keyword_type($state, $schema, 'boolean');
186             return E($state, 'use of exclusiveMinimum requires the presence of minimum')
187 16 50       82 if not exists $schema->{minimum};
188 16         65 return 1;
189             }
190              
191 496     496   1492 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         1434  
  496         1204  
  496         1346  
  496         1296  
  496         1043  
192             # we do the work in "minimum" for draft4 so we don't generate multiple errors
193 496 100       2521 return 1 if $state->{specification_version} eq 'draft4';
194              
195             return 1 if not is_type('number', $data)
196 485 50 66     2212 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
197              
198 256 100       1849 return 1 if 0+$data > $schema->{exclusiveMinimum};
199 138         24969 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   2167 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1948  
  666         2023  
  666         1656  
  666         1481  
  666         1422  
205 666 100       3219 return 1 if not is_type('string', $data);
206 417 100       3197 return 1 if length($data) <= $schema->{maxLength};
207 193         1262 return E($state, 'length is greater than %d', $schema->{maxLength});
208             }
209              
210             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
211              
212 621     621   1930 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1955  
  621         2060  
  621         1663  
  621         1518  
  621         1405  
213 621 100       3116 return 1 if not is_type('string', $data);
214 368 100       2861 return 1 if length($data) >= $schema->{minLength};
215 169         1206 return E($state, 'length is less than %d', $schema->{minLength});
216             }
217              
218 1288     1288   3657 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         3664  
  1288         3235  
  1288         5810  
  1288         3070  
219             return if not assert_keyword_type($state, $schema, 'string')
220 1288 100 66     6277 or not assert_pattern($state, $schema->{pattern});
221 1287         6448 return 1;
222             }
223              
224 1278     1278   3946 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         4083  
  1278         3738  
  1278         3521  
  1278         3497  
  1278         2949  
225 1278 100       6792 return 1 if not is_type('string', $data);
226              
227 989 100       19721 return 1 if $data =~ m/(?:$schema->{pattern})/;
228 452         3168 return E($state, 'pattern does not match');
229             }
230              
231             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
232              
233 488     488   1250 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         1419  
  488         1246  
  488         1134  
  488         1037  
  488         1101  
234 488 100       2130 return 1 if not is_type('array', $data);
235 292 100       1796 return 1 if @$data <= $schema->{maxItems};
236 136 100       1107 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   1536 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         1434  
  531         1391  
  531         1222  
  531         1332  
  531         1104  
242 531 100       2304 return 1 if not is_type('array', $data);
243 309 100       2203 return 1 if @$data >= $schema->{minItems};
244 142 100       1118 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
245             }
246              
247 1149     1149   3832 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         2862  
  1149         2661  
  1149         2563  
  1149         2080  
248 1149         5834 return assert_keyword_type($state, $schema, 'boolean');
249             }
250              
251 1120     1120   3166 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         2802  
  1120         2745  
  1120         2293  
  1120         2344  
  1120         2275  
252 1120 100       5627 return 1 if not is_type('array', $data);
253 908 100       8097 return 1 if not $schema->{uniqueItems};
254 628 100       11920 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
255 280         1823 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   1291 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         1164  
  399         1031  
  399         905  
  399         918  
  399         783  
267 399 100       1845 return 1 if not is_type('object', $data);
268 234 100       1811 return 1 if keys %$data <= $schema->{maxProperties};
269             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
270 112 100       974 $schema->{maxProperties} > 1 ? 'ies' : 'y');
271             }
272              
273             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
274              
275 403     403   1100 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         1137  
  403         967  
  403         882  
  403         940  
  403         811  
276 403 100       1640 return 1 if not is_type('object', $data);
277 238 100       1762 return 1 if keys %$data >= $schema->{minProperties};
278             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
279 116 100       927 $schema->{minProperties} > 1 ? 'ies' : 'y');
280             }
281              
282 2107     2107   4907 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         20348  
  2107         4062  
  2107         4128  
  2107         3813  
283 2107 50       8454 return if not assert_keyword_type($state, $schema, 'array');
284 2107 50 66     10140 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
285              
286 2107 50       14244 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       10040 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
293 2107         8805 return 1;
294             }
295              
296 2273     2273   10606 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         6211  
  2273         5236  
  2273         4507  
  2273         5143  
  2273         4237  
297 2273 100       11129 return 1 if not is_type('object', $data);
298              
299 2088         14922 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
300 2088 100       9837 return 1 if not @missing;
301 823 100       6700 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
302             }
303              
304 319     319   858 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         917  
  319         887  
  319         816  
  319         659  
305 319 50       1684 return if not assert_keyword_type($state, $schema, 'object');
306              
307 319         895 my $valid = 1;
308 319         2193 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
309             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
310 335 50       1693 if not is_type('array', $schema->{dependentRequired}{$property});
311              
312 335         2041 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
313             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
314 349 100       1793 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       2228 if not is_elements_unique($schema->{dependentRequired}{$property});
319             }
320 319         1435 return $valid;
321             }
322              
323 295     295   899 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         992  
  295         862  
  295         668  
  295         736  
  295         676  
324 295 100       1305 return 1 if not is_type('object', $data);
325              
326 185         579 my $valid = 1;
327 185         1312 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
328 201 100       1099 next if not exists $data->{$property};
329              
330 165 100       1657 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
331 85 100       2010 $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       1062 return 1 if $valid;
337 85         336 return E($state, 'not all dependencies are satisfied');
338             }
339              
340 1827     1827   5268 sub _assert_number ($class, $schema, $state) {
  1827         5248  
  1827         4763  
  1827         4075  
  1827         4498  
341 1827         9636 return assert_keyword_type($state, $schema, 'number');
342             }
343              
344 3683     3683   11103 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         10216  
  3683         8317  
  3683         7858  
  3683         7916  
345 3683 50       17130 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       22304 if $schema->{$state->{keyword}} < 0;
348 3683         75415 return 1;
349             }
350              
351             1;
352              
353             __END__