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   1573 use strict;
  38         77  
  38         1375  
2 38     38   184 use warnings;
  38         103  
  38         2720  
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.638';
8              
9 38     38   557 use 5.020;
  38         121  
10 38     38   151 use Moo;
  38         65  
  38         222  
11 38     38   12534 use strictures 2;
  38         257  
  38         1270  
12 38     38   13840 use stable 0.031 'postderef';
  38         500  
  38         239  
13 38     38   5777 use experimental 'signatures';
  38         90  
  38         152  
14 38     38   2043 no autovivification warn => qw(fetch store exists delete);
  38         95  
  38         258  
15 38     38   2451 use if "$]" >= 5.022, experimental => 're_strict';
  38         74  
  38         799  
16 38     38   2716 no if "$]" >= 5.031009, feature => 'indirect';
  38         77  
  38         2169  
17 38     38   176 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         71  
  38         1905  
18 38     38   173 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         109  
  38         2025  
19 38     38   195 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         80  
  38         2291  
20 38     38   165 no feature 'switch';
  38         94  
  38         1620  
21 38     38   148 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         87  
  38         1599  
22 38     38   154 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         77  
  38         603  
23 38     38   2480 use Scalar::Util 'looks_like_number';
  38         98  
  38         2964  
24 38     38   196 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         85  
  38         3148  
25 38     38   46741 use Math::BigFloat;
  38         2727513  
  38         190  
26 38     38   899807 use namespace::clean;
  38         79  
  38         413  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 39 sub vocabulary ($class) {
  22         44  
  22         40  
31 22         98 '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 26 sub evaluation_order ($class) { 1 }
  14         28  
  14         23  
  14         59  
36              
37 167     167 0 1897 sub keywords ($class, $spec_version) {
  167         294  
  167         510  
  167         295  
38             return (
39 167 100       10932 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   14119 sub _traverse_keyword_type ($class, $schema, $state) {
  8553         13360  
  8553         11695  
  8553         11058  
  8553         11963  
50 8553 100       25892 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       1445 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1128 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1250 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     6027  
57             }
58 360 50       1482 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8189 100       24392 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8185 100 50     16153 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38588   50     88296  
64             }
65 8541         21348 return 1;
66             }
67              
68 8951     8951   15692 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8951         13321  
  8951         13381  
  8951         12625  
  8951         12856  
  8951         11744  
69 8951 100       43023 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8951 100       44459 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8951 100       16626 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     89098 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       12671 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   1543 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         1368  
  862         1355  
  862         1331  
  862         1191  
83 862         2696 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   1459 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1414  
  770         1196  
  770         1173  
  770         1044  
  770         1028  
87 770         1113 my @s; my $idx = 0;
  770         1048  
88 770         8831 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       2187 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         23862  
90             return E($state, 'value does not match'
91 341 100       3011 .(!(grep $_->{path}, @s) ? ''
92             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
93             }
94              
95 1473     1473   2837 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1473         2665  
  1473         2599  
  1473         2348  
  1473         2267  
  1473         4238  
96              
97 1379     1379   3170 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1379         2618  
  1379         2481  
  1379         2108  
  1379         1985  
  1379         2057  
98 1379         5244 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
99 1379 100       6349 return 1 if is_equal($data, $schema->{const}, \%s);
100 651 100       10000 return E($state, 'value does not match'.($s{path} ? " (at '$s{path}': $s{error})" : ''));
101             }
102              
103 1119     1119   2059 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         2170  
  1119         1832  
  1119         1797  
  1119         1710  
104 1119 100       3713 return if not assert_keyword_type($state, $schema, 'number');
105 1117 50       3921 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
106 1117         42944 return 1;
107             }
108              
109 1126     1126   2098 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         1970  
  1126         1826  
  1126         1777  
  1126         1638  
  1126         1666  
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     3705 and do { $data = 0+$data; 1 });
  2   66     7  
  2   33     8  
      100        
113              
114 894         1618 my $remainder;
115              
116 894 100 100     2249 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
117 767         1978 $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       1567 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
122 127 100       10467 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
123 127         5786 $remainder = $dividend->bmod($divisor);
124             }
125              
126 894 100       53601 return 1 if $remainder == 0;
127 417         28498 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   1725 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         1750  
  825         1685  
  825         1503  
  825         1530  
  825         1314  
133             return 1 if not is_type('number', $data)
134 825 50 66     3089 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
135              
136 582 100       3007 return 1 if 0+$data < $schema->{maximum};
137 415 100 100     36474 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
138 6         85 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
139             }
140             else {
141 409 100       1738 return 1 if 0+$data == $schema->{maximum};
142 246         15568 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
143             }
144             }
145              
146 580     580   1141 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1103  
  580         980  
  580         1023  
  580         1022  
147 580 100       3358 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
148              
149 18 50       36 return if not assert_keyword_type($state, $schema, 'boolean');
150             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
151 18 50       47 if not exists $schema->{maximum};
152 18         40 return 1;
153             }
154              
155 563     563   1258 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1102  
  563         1129  
  563         1267  
  563         984  
  563         940  
156             # we do the work in "maximum" for draft4 so we don't generate multiple errors
157 563 100       2087 return 1 if $state->{specification_version} eq 'draft4';
158              
159             return 1 if not is_type('number', $data)
160 548 50 66     1874 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161              
162 319 100       1606 return 1 if 0+$data < $schema->{exclusiveMaximum};
163 171         16989 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   1707 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         1774  
  946         1572  
  946         1477  
  946         1524  
  946         1369  
169             return 1 if not is_type('number', $data)
170 946 50 66     3371 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
171              
172 687 100       2934 return 1 if 0+$data > $schema->{minimum};
173 511 100 100     56336 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
174 4         52 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
175             }
176             else {
177 507 100       1850 return 1 if 0+$data == $schema->{minimum};
178 323         30573 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
179             }
180             }
181              
182 515     515   1116 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         1105  
  515         952  
  515         863  
  515         914  
183 515 100       3250 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       53 if not exists $schema->{minimum};
188 16         38 return 1;
189             }
190              
191 496     496   966 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         1107  
  496         893  
  496         823  
  496         820  
  496         752  
192             # we do the work in "minimum" for draft4 so we don't generate multiple errors
193 496 100       2139 return 1 if $state->{specification_version} eq 'draft4';
194              
195             return 1 if not is_type('number', $data)
196 485 50 66     1794 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
197              
198 256 100       1378 return 1 if 0+$data > $schema->{exclusiveMinimum};
199 138         14487 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   1788 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1571  
  666         1443  
  666         1330  
  666         1148  
  666         1143  
205 666 100       2289 return 1 if not is_type('string', $data);
206 417 100       2262 return 1 if length($data) <= $schema->{maxLength};
207 193         900 return E($state, 'length is greater than %d', $schema->{maxLength});
208             }
209              
210             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
211              
212 621     621   1384 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1336  
  621         1301  
  621         1275  
  621         1070  
  621         1051  
213 621 100       2138 return 1 if not is_type('string', $data);
214 368 100       2107 return 1 if length($data) >= $schema->{minLength};
215 169         803 return E($state, 'length is less than %d', $schema->{minLength});
216             }
217              
218 1288     1288   2781 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         2595  
  1288         2248  
  1288         2424  
  1288         2113  
219             return if not assert_keyword_type($state, $schema, 'string')
220 1288 100 66     4614 or not assert_pattern($state, $schema->{pattern});
221 1287         4824 return 1;
222             }
223              
224 1278     1278   2777 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         2578  
  1278         3007  
  1278         2467  
  1278         2278  
  1278         2257  
225 1278 100       4340 return 1 if not is_type('string', $data);
226              
227 989 100       14277 return 1 if $data =~ m/(?:$schema->{pattern})/;
228 452         1874 return E($state, 'pattern does not match');
229             }
230              
231             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
232              
233 488     488   1039 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         1128  
  488         1016  
  488         1019  
  488         885  
  488         799  
234 488 100       1766 return 1 if not is_type('array', $data);
235 292 100       1490 return 1 if @$data <= $schema->{maxItems};
236 136 100       917 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   1058 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         1040  
  531         947  
  531         919  
  531         859  
  531         868  
242 531 100       1629 return 1 if not is_type('array', $data);
243 309 100       1570 return 1 if @$data >= $schema->{minItems};
244 142 100       825 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
245             }
246              
247 1149     1149   2366 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         2091  
  1149         1985  
  1149         1666  
  1149         1600  
248 1149         3939 return assert_keyword_type($state, $schema, 'boolean');
249             }
250              
251 1120     1120   2118 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         2006  
  1120         1881  
  1120         1795  
  1120         1885  
  1120         1654  
252 1120 100       3857 return 1 if not is_type('array', $data);
253 908 100       5521 return 1 if not $schema->{uniqueItems};
254 628 100       8051 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
255 280         1288 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   857 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         714  
  399         776  
  399         743  
  399         631  
  399         559  
267 399 100       1196 return 1 if not is_type('object', $data);
268 234 100       1279 return 1 if keys %$data <= $schema->{maxProperties};
269             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
270 112 100       1126 $schema->{maxProperties} > 1 ? 'ies' : 'y');
271             }
272              
273             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
274              
275 403     403   780 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         821  
  403         742  
  403         671  
  403         636  
  403         660  
276 403 100       1195 return 1 if not is_type('object', $data);
277 238 100       1242 return 1 if keys %$data >= $schema->{minProperties};
278             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
279 116 100       740 $schema->{minProperties} > 1 ? 'ies' : 'y');
280             }
281              
282 2107     2107   3505 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         3457  
  2107         3181  
  2107         3099  
  2107         2894  
283 2107 50       5725 return if not assert_keyword_type($state, $schema, 'array');
284 2107 50 66     7089 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
285              
286 2107 50       10421 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       6800 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
293 2107         10596 return 1;
294             }
295              
296 2273     2273   4593 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         4045  
  2273         3755  
  2273         3761  
  2273         3423  
  2273         3126  
297 2273 100       7048 return 1 if not is_type('object', $data);
298              
299 2088         9816 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
300 2088 100       6819 return 1 if not @missing;
301 823 100       4837 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
302             }
303              
304 319     319   619 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         694  
  319         562  
  319         579  
  319         569  
305 319 50       1101 return if not assert_keyword_type($state, $schema, 'object');
306              
307 319         659 my $valid = 1;
308 319         1525 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
309             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
310 335 50       1173 if not is_type('array', $schema->{dependentRequired}{$property});
311              
312 335         1458 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
313             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
314 349 100       1241 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       1468 if not is_elements_unique($schema->{dependentRequired}{$property});
319             }
320 319         1040 return $valid;
321             }
322              
323 295     295   643 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         634  
  295         619  
  295         468  
  295         528  
  295         527  
324 295 100       955 return 1 if not is_type('object', $data);
325              
326 185         383 my $valid = 1;
327 185         1425 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
328 201 100       725 next if not exists $data->{$property};
329              
330 165 100       1169 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
331 85 100       1498 $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       764 return 1 if $valid;
337 85         241 return E($state, 'not all dependencies are satisfied');
338             }
339              
340 1827     1827   3866 sub _assert_number ($class, $schema, $state) {
  1827         3802  
  1827         3364  
  1827         3242  
  1827         2958  
341 1827         6511 return assert_keyword_type($state, $schema, 'number');
342             }
343              
344 3683     3683   7440 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         7418  
  3683         7016  
  3683         7834  
  3683         6286  
345 3683 50       12124 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       14587 if $schema->{$state->{keyword}} < 0;
348 3683         56134 return 1;
349             }
350              
351             1;
352              
353             __END__