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   1213 use strict;
  38         79  
  38         1238  
2 38     38   153 use warnings;
  38         58  
  38         2518  
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.641';
8              
9 38     38   489 use 5.020;
  38         112  
10 38     38   133 use Moo;
  38         60  
  38         195  
11 38     38   10929 use strictures 2;
  38         243  
  38         1120  
12 38     38   12010 use stable 0.031 'postderef';
  38         545  
  38         264  
13 38     38   5167 use experimental 'signatures';
  38         69  
  38         196  
14 38     38   1770 no autovivification warn => qw(fetch store exists delete);
  38         99  
  38         239  
15 38     38   2325 use if "$]" >= 5.022, experimental => 're_strict';
  38         60  
  38         689  
16 38     38   2456 no if "$]" >= 5.031009, feature => 'indirect';
  38         63  
  38         1939  
17 38     38   193 no if "$]" >= 5.033001, feature => 'multidimensional';
  38         66  
  38         1607  
18 38     38   176 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  38         295  
  38         2133  
19 38     38   188 no if "$]" >= 5.041009, feature => 'smartmatch';
  38         63  
  38         1239  
20 38     38   190 no feature 'switch';
  38         88  
  38         1291  
21 38     38   134 use if "$]" < 5.041010, 'List::Util' => 'any';
  38         63  
  38         1283  
22 38     38   133 use if "$]" >= 5.041010, experimental => 'keyword_any';
  38         88  
  38         406  
23 38     38   2131 use Scalar::Util 'looks_like_number';
  38         84  
  38         2632  
24 38     38   161 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         60  
  38         3005  
25 38     38   43185 use Math::BigFloat;
  38         2553610  
  38         591  
26 38     38   842785 use namespace::clean;
  38         86  
  38         422  
27              
28             with 'JSON::Schema::Modern::Vocabulary';
29              
30 22     22 0 54 sub vocabulary ($class) {
  22         42  
  22         60  
31 22         99 '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 56 sub evaluation_order ($class) { 1 }
  14         21  
  14         20  
  14         49  
36              
37 167     167 0 1122 sub keywords ($class, $spec_version) {
  167         253  
  167         259  
  167         264  
38             return (
39 167 100       10049 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   12956 sub _traverse_keyword_type ($class, $schema, $state) {
  8553         11800  
  8553         12892  
  8553         10224  
  8553         10382  
50 8553 100       24343 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       1358 return E($state, 'type array is empty') if not $schema->{type}->@*;
53              
54 364         1143 foreach my $type ($schema->{type}->@*) {
55             return E($state, 'unrecognized type "%s"', $type//'')
56 758 100 50     1192 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2906   100     5920  
57             }
58 360 50       1442 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
59             }
60             else {
61 8189 100       22077 return if not assert_keyword_type($state, $schema, 'string');
62             return E($state, 'unrecognized type "%s"', $schema->{type}//'')
63 8185 100 50     16092 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  38588   50     83871  
64             }
65 8541         19526 return 1;
66             }
67              
68 8951     8951   15446 sub _eval_keyword_type ($class, $data, $schema, $state) {
  8951         14394  
  8951         13620  
  8951         12204  
  8951         12581  
  8951         10546  
69 8951 100       40150 my $type = get_type($data, $state->{specification_version} eq 'draft4' ? { legacy_ints => 1 } : ());
70 8951 100       41462 my @want = ref $schema->{type} eq 'ARRAY' ? $schema->{type}->@* : $schema->{type};
71              
72 8951 100       15703 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     85626 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       12117 return E($state, 'got %s, not %s%s', $type, (@want > 1 ? 'one of ' : ''), join(', ', @want));
80             }
81              
82 862     862   1383 sub _traverse_keyword_enum ($class, $schema, $state) {
  862         1361  
  862         1398  
  862         1097  
  862         978  
83 862         2413 return assert_keyword_type($state, $schema, 'array');
84             }
85              
86 770     770   1312 sub _eval_keyword_enum ($class, $data, $schema, $state) {
  770         1191  
  770         1088  
  770         988  
  770         1025  
  770         897  
87 770         1124 my @s; my $idx = 0;
  770         1041  
88 770         2716 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
89 770 100       2137 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1617         21265  
90             return E($state, 'value does not match ('
91 341 100       4450 .(join('; ', map "from enum $_".(length $s[$_]->{path} ? " at '$s[$_]->{path}'" : '').": $s[$_]->{error}", 0..$#s).')'));
92             }
93              
94 1473     1473   2536 sub _traverse_keyword_const ($class, $schema, $state) { 1 }
  1473         2797  
  1473         2399  
  1473         2257  
  1473         2160  
  1473         3518  
95              
96 1379     1379   2845 sub _eval_keyword_const ($class, $data, $schema, $state) {
  1379         2803  
  1379         2508  
  1379         2173  
  1379         2055  
  1379         1956  
97 1379         5653 my %s = $state->%{qw(scalarref_booleans stringy_numbers)};
98 1379 100       5905 return 1 if is_equal($data, $schema->{const}, \%s);
99 651 100       10476 return E($state, 'value does not match ('.($s{path} ? "at '$s{path}': " : '').$s{error}.')');
100             }
101              
102 1119     1119   1931 sub _traverse_keyword_multipleOf ($class, $schema, $state) {
  1119         2006  
  1119         1721  
  1119         1781  
  1119         1758  
103 1119 100       3328 return if not assert_keyword_type($state, $schema, 'number');
104 1117 50       3820 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
105 1117         45580 return 1;
106             }
107              
108 1126     1126   2000 sub _eval_keyword_multipleOf ($class, $data, $schema, $state) {
  1126         1987  
  1126         1785  
  1126         1709  
  1126         1588  
  1126         1605  
109             return 1 if not is_type('number', $data)
110             and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data)
111 1126 50 66     3503 and do { $data = 0+$data; 1 });
  2   66     8  
  2   33     8  
      100        
112              
113 894         1561 my $remainder;
114              
115 894 100 100     2032 if (get_type($data) eq 'integer' and get_type($schema->{multipleOf}) eq 'integer') {
116 767         1965 $remainder = $data % $schema->{multipleOf};
117             }
118             else {
119             # if either value is a float, use the bignum library for the calculation for an accurate remainder
120 127 100       1591 my $dividend = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
121 127 100       10495 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
122 127         6593 $remainder = $dividend->bmod($divisor);
123             }
124              
125 894 100       54640 return 1 if $remainder == 0;
126 417         19529 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
127             }
128              
129             *_traverse_keyword_maximum = \&_assert_number;
130              
131 825     825   1752 sub _eval_keyword_maximum ($class, $data, $schema, $state) {
  825         1851  
  825         1380  
  825         1295  
  825         1218  
  825         1293  
132             return 1 if not is_type('number', $data)
133 825 50 66     2973 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
134              
135 582 100       2779 return 1 if 0+$data < $schema->{maximum};
136 415 100 100     34429 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMaximum}) {
137 6         71 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{maximum}));
138             }
139             else {
140 409 100       1582 return 1 if 0+$data == $schema->{maximum};
141 246         14953 return E($state, 'value is greater than %s', sprintf_num($schema->{maximum}));
142             }
143             }
144              
145 580     580   1167 sub _traverse_keyword_exclusiveMaximum ($class, $schema, $state) {
  580         1205  
  580         1055  
  580         973  
  580         932  
146 580 100       3282 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
147              
148 18 50       43 return if not assert_keyword_type($state, $schema, 'boolean');
149             return E($state, 'use of exclusiveMaximum requires the presence of maximum')
150 18 50       52 if not exists $schema->{maximum};
151 18         38 return 1;
152             }
153              
154 563     563   1200 sub _eval_keyword_exclusiveMaximum ($class, $data, $schema, $state) {
  563         1056  
  563         1021  
  563         967  
  563         873  
  563         865  
155             # we do the work in "maximum" for draft4 so we don't generate multiple errors
156 563 100       2016 return 1 if $state->{specification_version} eq 'draft4';
157              
158             return 1 if not is_type('number', $data)
159 548 50 66     1811 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
160              
161 319 100       1932 return 1 if 0+$data < $schema->{exclusiveMaximum};
162 171         16258 return E($state, 'value is greater than or equal to %s', sprintf_num($schema->{exclusiveMaximum}));
163             }
164              
165             *_traverse_keyword_minimum = \&_assert_number;
166              
167 946     946   1603 sub _eval_keyword_minimum ($class, $data, $schema, $state) {
  946         1709  
  946         1515  
  946         1388  
  946         1431  
  946         1282  
168             return 1 if not is_type('number', $data)
169 946 50 66     2837 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
170              
171 687 100       2801 return 1 if 0+$data > $schema->{minimum};
172 511 100 100     51811 if ($state->{specification_version} eq 'draft4' and $schema->{exclusiveMinimum}) {
173 4         50 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{minimum}));
174             }
175             else {
176 507 100       1841 return 1 if 0+$data == $schema->{minimum};
177 323         28934 return E($state, 'value is less than %s', sprintf_num($schema->{minimum}));
178             }
179             }
180              
181 515     515   980 sub _traverse_keyword_exclusiveMinimum ($class, $schema, $state) {
  515         933  
  515         919  
  515         897  
  515         888  
182 515 100       3052 return assert_keyword_type($state, $schema, 'number') if $state->{specification_version} ne 'draft4';
183              
184 16 50       30 return if not assert_keyword_type($state, $schema, 'boolean');
185             return E($state, 'use of exclusiveMinimum requires the presence of minimum')
186 16 50       46 if not exists $schema->{minimum};
187 16         36 return 1;
188             }
189              
190 496     496   935 sub _eval_keyword_exclusiveMinimum ($class, $data, $schema, $state) {
  496         953  
  496         888  
  496         866  
  496         715  
  496         774  
191             # we do the work in "minimum" for draft4 so we don't generate multiple errors
192 496 100       1788 return 1 if $state->{specification_version} eq 'draft4';
193              
194             return 1 if not is_type('number', $data)
195 485 50 66     1529 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
196              
197 256 100       1227 return 1 if 0+$data > $schema->{exclusiveMinimum};
198 138         14437 return E($state, 'value is less than or equal to %s', sprintf_num($schema->{exclusiveMinimum}));
199             }
200              
201             *_traverse_keyword_maxLength = \&_assert_non_negative_integer;
202              
203 666     666   1324 sub _eval_keyword_maxLength ($class, $data, $schema, $state) {
  666         1397  
  666         1310  
  666         1114  
  666         1112  
  666         1352  
204 666 100       2247 return 1 if not is_type('string', $data);
205 417 100       1956 return 1 if length($data) <= $schema->{maxLength};
206 193         840 return E($state, 'length is greater than %d', $schema->{maxLength});
207             }
208              
209             *_traverse_keyword_minLength = \&_assert_non_negative_integer;
210              
211 621     621   1205 sub _eval_keyword_minLength ($class, $data, $schema, $state) {
  621         1462  
  621         1164  
  621         997  
  621         942  
  621         886  
212 621 100       1936 return 1 if not is_type('string', $data);
213 368 100       1759 return 1 if length($data) >= $schema->{minLength};
214 169         729 return E($state, 'length is less than %d', $schema->{minLength});
215             }
216              
217 1288     1288   2517 sub _traverse_keyword_pattern ($class, $schema, $state) {
  1288         2450  
  1288         2374  
  1288         2028  
  1288         1928  
218             return if not assert_keyword_type($state, $schema, 'string')
219 1288 100 66     4107 or not assert_pattern($state, $schema->{pattern});
220 1287         3898 return 1;
221             }
222              
223 1278     1278   2532 sub _eval_keyword_pattern ($class, $data, $schema, $state) {
  1278         2565  
  1278         2281  
  1278         2866  
  1278         1856  
  1278         2304  
224 1278 100       3910 return 1 if not is_type('string', $data);
225              
226 989 100       13495 return 1 if $data =~ m/(?:$schema->{pattern})/;
227 452         1696 return E($state, 'pattern does not match');
228             }
229              
230             *_traverse_keyword_maxItems = \&_assert_non_negative_integer;
231              
232 488     488   1070 sub _eval_keyword_maxItems ($class, $data, $schema, $state) {
  488         903  
  488         815  
  488         1153  
  488         758  
  488         751  
233 488 100       1436 return 1 if not is_type('array', $data);
234 292 100       1323 return 1 if @$data <= $schema->{maxItems};
235 136 100       870 return E($state, 'array has more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
236             }
237              
238             *_traverse_keyword_minItems = \&_assert_non_negative_integer;
239              
240 531     531   998 sub _eval_keyword_minItems ($class, $data, $schema, $state) {
  531         1060  
  531         901  
  531         851  
  531         922  
  531         846  
241 531 100       1563 return 1 if not is_type('array', $data);
242 309 100       1544 return 1 if @$data >= $schema->{minItems};
243 142 100       861 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
244             }
245              
246 1149     1149   2001 sub _traverse_keyword_uniqueItems ($class, $schema, $state) {
  1149         1837  
  1149         1563  
  1149         1599  
  1149         1526  
247 1149         3570 return assert_keyword_type($state, $schema, 'boolean');
248             }
249              
250 1120     1120   1985 sub _eval_keyword_uniqueItems ($class, $data, $schema, $state) {
  1120         1778  
  1120         1643  
  1120         1625  
  1120         1498  
  1120         1576  
251 1120 100       3348 return 1 if not is_type('array', $data);
252 908 100       4614 return 1 if not $schema->{uniqueItems};
253 628 100       7333 return 1 if is_elements_unique($data, my $s = +{ $state->%{qw(scalarref_booleans stringy_numbers)}, equal_indices => [] });
254 280         1235 return E($state, 'items at indices %d and %d are not unique', $s->{equal_indices}->@*);
255             }
256              
257             # The evaluation implementations of maxContains and minContains are in the Applicator vocabulary,
258             # as 'contains' needs to run first
259             *_traverse_keyword_maxContains = \&_assert_non_negative_integer;
260              
261             *_traverse_keyword_minContains = \&_assert_non_negative_integer;
262              
263             *_traverse_keyword_maxProperties = \&_assert_non_negative_integer;
264              
265 399     399   806 sub _eval_keyword_maxProperties ($class, $data, $schema, $state) {
  399         806  
  399         668  
  399         643  
  399         686  
  399         630  
266 399 100       1096 return 1 if not is_type('object', $data);
267 234 100       1106 return 1 if keys %$data <= $schema->{maxProperties};
268             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
269 112 100       726 $schema->{maxProperties} > 1 ? 'ies' : 'y');
270             }
271              
272             *_traverse_keyword_minProperties = \&_assert_non_negative_integer;
273              
274 403     403   844 sub _eval_keyword_minProperties ($class, $data, $schema, $state) {
  403         800  
  403         752  
  403         678  
  403         625  
  403         602  
275 403 100       1084 return 1 if not is_type('object', $data);
276 238 100       1277 return 1 if keys %$data >= $schema->{minProperties};
277             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
278 116 100       683 $schema->{minProperties} > 1 ? 'ies' : 'y');
279             }
280              
281 2107     2107   3432 sub _traverse_keyword_required ($class, $schema, $state) {
  2107         3251  
  2107         2926  
  2107         3132  
  2107         2657  
282 2107 50       5450 return if not assert_keyword_type($state, $schema, 'array');
283 2107 50 66     7480 return E($state, '"required" array is empty') if $state->{specification_version} eq 'draft4' and not $schema->{required}->@*;
284              
285 2107 50       9597 if (my @non_string = grep !is_type('string', $schema->{required}->[$_]), 0 .. $schema->{required}->$#*) {
286             ()= E({ %$state, _keyword_path_suffix => $_ }, '"required" element is not a string')
287 0         0 foreach @non_string;
288 0         0 return;
289             }
290              
291 2107 50       6729 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
292 2107         5388 return 1;
293             }
294              
295 2273     2273   4228 sub _eval_keyword_required ($class, $data, $schema, $state) {
  2273         4131  
  2273         3490  
  2273         3285  
  2273         3233  
  2273         2864  
296 2273 100       6683 return 1 if not is_type('object', $data);
297              
298 2088         9620 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
299 2088 100       6713 return 1 if not @missing;
300 823 100       4668 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
301             }
302              
303 319     319   653 sub _traverse_keyword_dependentRequired ($class, $schema, $state) {
  319         595  
  319         477  
  319         490  
  319         438  
304 319 50       897 return if not assert_keyword_type($state, $schema, 'object');
305              
306 319         532 my $valid = 1;
307 319         1410 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
308             $valid = E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
309 335 50       1006 if not is_type('array', $schema->{dependentRequired}{$property});
310              
311 335         1378 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
312             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element is not a string')
313 349 100       1061 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
314             }
315              
316             $valid = E({ %$state, _keyword_path_suffix => $property }, 'elements are not unique')
317 335 50       1239 if not is_elements_unique($schema->{dependentRequired}{$property});
318             }
319 319         894 return $valid;
320             }
321              
322 295     295   614 sub _eval_keyword_dependentRequired ($class, $data, $schema, $state) {
  295         520  
  295         513  
  295         460  
  295         463  
  295         414  
323 295 100       848 return 1 if not is_type('object', $data);
324              
325 185         344 my $valid = 1;
326 185         886 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
327 201 100       692 next if not exists $data->{$property};
328              
329 165 100       900 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
330 85 100       1295 $valid = E({ %$state, _keyword_path_suffix => $property },
331             'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
332             }
333             }
334              
335 185 100       657 return 1 if $valid;
336 85         582 return E($state, 'not all dependencies are satisfied');
337             }
338              
339 1827     1827   3505 sub _assert_number ($class, $schema, $state) {
  1827         3491  
  1827         3122  
  1827         2936  
  1827         2831  
340 1827         5927 return assert_keyword_type($state, $schema, 'number');
341             }
342              
343 3683     3683   7092 sub _assert_non_negative_integer ($class, $schema, $state) {
  3683         7058  
  3683         6313  
  3683         6340  
  3683         5730  
344 3683 50       10966 return if not assert_keyword_type($state, $schema, 'integer');
345             return E($state, '%s value is not a non-negative integer', $state->{keyword})
346 3683 50       15218 if $schema->{$state->{keyword}} < 0;
347 3683         50141 return 1;
348             }
349              
350             1;
351              
352             __END__