File Coverage

blib/lib/JSON/Schema/Modern/Vocabulary/Validation.pm
Criterion Covered Total %
statement 361 361 100.0
branch 142 166 85.5
condition 98 125 78.4
subroutine 68 68 100.0
pod 0 3 0.0
total 669 723 92.5


line stmt bran cond sub pod time code
1 31     31   18404 use strict;
  31         93  
  31         1126  
2 31     31   187 use warnings;
  31         109  
  31         1868  
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.571';
8              
9 31     31   580 use 5.020;
  31         129  
10 31     31   180 use Moo;
  31         82  
  31         213  
11 31     31   11550 use strictures 2;
  31         268  
  31         1330  
12 31     31   5920 use stable 0.031 'postderef';
  31         593  
  31         203  
13 31     31   4662 use experimental 'signatures';
  31         102  
  31         210  
14 31     31   2803 use if "$]" >= 5.022, experimental => 're_strict';
  31         118  
  31         314  
15 31     31   3228 no if "$]" >= 5.031009, feature => 'indirect';
  31         102  
  31         277  
16 31     31   1709 no if "$]" >= 5.033001, feature => 'multidimensional';
  31         118  
  31         240  
17 31     31   1693 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  31         114  
  31         298  
18 31     31   1379 use List::Util 'any';
  31         122  
  31         2694  
19 31     31   257 use Ref::Util 0.100 'is_plain_arrayref';
  31         617  
  31         1735  
20 31     31   233 use Scalar::Util 'looks_like_number';
  31         99  
  31         2388  
21 31     31   250 use if "$]" >= 5.022, POSIX => 'isinf';
  31         110  
  31         267  
22 31     31   60172 use JSON::Schema::Modern::Utilities qw(is_type get_type is_equal is_elements_unique E assert_keyword_type assert_pattern jsonp sprintf_num);
  31         113  
  31         2945  
23 31     31   34105 use Math::BigFloat;
  31         1677892  
  31         238  
24 31     31   708344 use namespace::clean;
  31         100  
  31         369  
25              
26             with 'JSON::Schema::Modern::Vocabulary';
27              
28             sub vocabulary {
29 15     15 0 89 'https://json-schema.org/draft/2019-09/vocab/validation' => 'draft2019-09',
30             'https://json-schema.org/draft/2020-12/vocab/validation' => 'draft2020-12';
31             }
32              
33 6     6 0 36 sub evaluation_order { 1 }
34              
35 92     92 0 243 sub keywords ($self, $spec_version) {
  92         225  
  92         210  
  92         167  
36             return (
37 92 100       6980 qw(type enum const
    100          
38             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
39             maxLength minLength pattern
40             maxItems minItems uniqueItems),
41             $spec_version ne 'draft7' ? qw(maxContains minContains) : (),
42             qw(maxProperties minProperties required),
43             $spec_version ne 'draft7' ? 'dependentRequired' : (),
44             );
45             }
46              
47 6021     6021   10892 sub _traverse_keyword_type ($self, $schema, $state) {
  6021         10504  
  6021         9967  
  6021         9703  
  6021         9147  
48 6021 100       15790 if (is_plain_arrayref($schema->{type})) {
49 279 50       1003 return E($state, 'type array is empty') if not $schema->{type}->@*;
50 279         803 foreach my $type ($schema->{type}->@*) {
51             return E($state, 'unrecognized type "%s"', $type//'<null>')
52 576 100 50 2123   2685 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2123   100     5607  
53             }
54 277 50       1221 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
55             }
56             else {
57 5742 100       16729 return if not assert_keyword_type($state, $schema, 'string');
58             return E($state, 'unrecognized type "%s"', $schema->{type}//'<null>')
59 5740 100 50 26691   35858 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  26691   50     64305  
60             }
61 6013         28307 return 1;
62             }
63              
64 7006     7006   12512 sub _eval_keyword_type ($self, $data, $schema, $state) {
  7006         11927  
  7006         12727  
  7006         11707  
  7006         10955  
  7006         10331  
65 7006         20288 my $type = get_type($data);
66 7006 100       27175 if (is_plain_arrayref($schema->{type})) {
67             return 1 if any {
68             $type eq $_ or ($_ eq 'number' and $type eq 'integer')
69             or ($type eq 'string' and $state->{stringy_numbers} and looks_like_number($data)
70             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
71 1696 50 100 1696   16008 or ($_ eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR')
      100        
      66        
      66        
      100        
      100        
      66        
      66        
      33        
72 1522 100       11221 } $schema->{type}->@*;
73 113         1074 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
74             }
75             else {
76             return 1 if $type eq $schema->{type} or ($schema->{type} eq 'number' and $type eq 'integer')
77             or ($type eq 'string' and $state->{stringy_numbers} and looks_like_number($data)
78             and ($schema->{type} eq 'number' or ($schema->{type} eq 'integer' and $data == int($data))))
79 5484 100 100     37819 or ($schema->{type} eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR');
      100        
      100        
      100        
      66        
      66        
      66        
      100        
      100        
      66        
80 1443         5163 return E($state, 'got %s, not %s', $type, $schema->{type});
81             }
82             }
83              
84 499     499   983 sub _traverse_keyword_enum ($self, $schema, $state) {
  499         918  
  499         1058  
  499         820  
  499         787  
85 499 50       1501 return if not assert_keyword_type($state, $schema, 'array');
86 499         1498 return 1;
87             }
88              
89 456     456   983 sub _eval_keyword_enum ($self, $data, $schema, $state) {
  456         870  
  456         1203  
  456         809  
  456         716  
  456         694  
90 456         821 my @s; my $idx = 0;
  456         797  
91 456         1510 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
92 456 100   1117   3580 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1117         13070  
93             return E($state, 'value does not match'
94             .(!(grep $_->{path}, @s) ? ''
95 188 100       1947 : ' (differences start '.join(', ', map 'from item #'.$_.' at "'.$s[$_]->{path}.'"', 0..$#s).')'));
96             }
97              
98 1241     1241   3593 sub _traverse_keyword_const { 1 }
99              
100 1127     1127   2116 sub _eval_keyword_const ($self, $data, $schema, $state) {
  1127         2179  
  1127         2124  
  1127         2089  
  1127         1864  
  1127         1850  
101 1127         3345 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
102 1127 100       5471 return 1 if is_equal($data, $schema->{const}, my $s = { scalarref_booleans => $state->{scalarref_booleans} });
103             return E($state, 'value does not match'
104 530 100       6704 .($s->{path} ? ' (differences start at "'.$s->{path}.'")' : ''));
105             }
106              
107 903     903   1756 sub _traverse_keyword_multipleOf ($self, $schema, $state) {
  903         1670  
  903         1619  
  903         1497  
  903         1395  
108 903 100       2697 return if not assert_keyword_type($state, $schema, 'number');
109 901 50       3093 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
110 901         14212 return 1;
111             }
112              
113 906     906   1771 sub _eval_keyword_multipleOf ($self, $data, $schema, $state) {
  906         1744  
  906         1832  
  906         1572  
  906         1485  
  906         1442  
114             return 1 if not is_type('number', $data)
115             and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data)
116 906 50 66     2949 and do { $data = 0+$data; 1 });
  2   66     14  
  2   33     9  
      100        
117              
118             # if either value is a float, use the bignum library for the calculation for an accurate remainder
119 700 100 100     4822 if (ref($data) =~ /^Math::Big(?:Int|Float)$/
      66        
      100        
120             or ref($schema->{multipleOf}) =~ /^Math::Big(?:Int|Float)$/
121             or get_type($data) eq 'number' or get_type($schema->{multipleOf}) eq 'number') {
122 52 100       389 $data = ref($data) =~ /^Math::Big(?:Int|Float)$/ ? $data->copy : Math::BigFloat->new($data);
123 52 100       3679 my $divisor = ref($schema->{multipleOf}) =~ /^Math::Big(?:Int|Float)$/ ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
124 52         1404 my ($quotient, $remainder) = $data->bdiv($divisor);
125 52 50       59089 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
126 52 100       621 return 1 if $remainder == 0;
127             }
128             else {
129 648         2098 my $quotient = $data / $schema->{multipleOf};
130 648 50       4434 return E($state, 'overflow while calculating quotient of integers')
    50          
131             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
132 648 100       3092 return 1 if int($quotient) == $quotient;
133             }
134              
135 326         6242 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
136             }
137              
138 769     769   3394 sub _traverse_keyword_maximum { goto \&_assert_number }
139              
140 691     691   1324 sub _eval_keyword_maximum ($self, $data, $schema, $state) {
  691         1355  
  691         1302  
  691         1189  
  691         1330  
  691         1129  
141             return 1 if not is_type('number', $data)
142 691 50 66     2204 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
143 480 100       2400 return 1 if 0+$data <= $schema->{maximum};
144 214         8367 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
145             }
146              
147 550     550   2367 sub _traverse_keyword_exclusiveMaximum { goto \&_assert_number }
148              
149 514     514   1047 sub _eval_keyword_exclusiveMaximum ($self, $data, $schema, $state) {
  514         959  
  514         894  
  514         1080  
  514         877  
  514         825  
150             return 1 if not is_type('number', $data)
151 514 50 66     1526 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
152 309 100       1535 return 1 if 0+$data < $schema->{exclusiveMaximum};
153 163         10364 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
154             }
155              
156 787     787   3219 sub _traverse_keyword_minimum { goto \&_assert_number }
157              
158 767     767   1532 sub _eval_keyword_minimum ($self, $data, $schema, $state) {
  767         1502  
  767         1376  
  767         1354  
  767         1318  
  767         1186  
159             return 1 if not is_type('number', $data)
160 767 50 66     2450 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161 547 100       2682 return 1 if 0+$data >= $schema->{minimum};
162 266         18093 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
163             }
164              
165 489     489   2381 sub _traverse_keyword_exclusiveMinimum { goto \&_assert_number }
166              
167 453     453   935 sub _eval_keyword_exclusiveMinimum ($self, $data, $schema, $state) {
  453         938  
  453         812  
  453         778  
  453         761  
  453         782  
168             return 1 if not is_type('number', $data)
169 453 50 66     1532 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
170 248 100       1324 return 1 if 0+$data > $schema->{exclusiveMinimum};
171 132         9025 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
172             }
173              
174 642     642   2882 sub _traverse_keyword_maxLength { goto \&_assert_non_negative_integer }
175              
176 585     585   1259 sub _eval_keyword_maxLength ($self, $data, $schema, $state) {
  585         1172  
  585         1139  
  585         1036  
  585         995  
  585         1018  
177 585 100       1807 return 1 if not is_type('string', $data);
178 360 100       1857 return 1 if length($data) <= $schema->{maxLength};
179 170         788 return E($state, 'length is greater than %d', $schema->{maxLength});
180             }
181              
182 603     603   2444 sub _traverse_keyword_minLength { goto \&_assert_non_negative_integer }
183              
184 553     553   1074 sub _eval_keyword_minLength ($self, $data, $schema, $state) {
  553         1104  
  553         1096  
  553         973  
  553         923  
  553         957  
185              
186 553 100       1637 return 1 if not is_type('string', $data);
187 327 100       1784 return 1 if length($data) >= $schema->{minLength};
188 154         654 return E($state, 'length is less than %d', $schema->{minLength});
189             }
190              
191 986     986   1865 sub _traverse_keyword_pattern ($self, $schema, $state) {
  986         1829  
  986         1686  
  986         1623  
  986         1527  
192             return if not assert_keyword_type($state, $schema, 'string')
193 986 100 66     2854 or not assert_pattern($state, $schema->{pattern});
194 985         3349 return 1;
195             }
196              
197 1028     1028   2090 sub _eval_keyword_pattern ($self, $data, $schema, $state) {
  1028         2021  
  1028         1870  
  1028         1782  
  1028         1821  
  1028         1738  
198 1028 100       3223 return 1 if not is_type('string', $data);
199              
200 785 100       9446 return 1 if $data =~ m/(?:$schema->{pattern})/;
201 368         1453 return E($state, 'pattern does not match');
202             }
203              
204 512     512   1957 sub _traverse_keyword_maxItems { goto \&_assert_non_negative_integer }
205              
206 441     441   811 sub _eval_keyword_maxItems ($self, $data, $schema, $state) {
  441         844  
  441         755  
  441         756  
  441         735  
  441         710  
207 441 100       1218 return 1 if not is_type('array', $data);
208 264 100       1151 return 1 if @$data <= $schema->{maxItems};
209 126 100       704 return E($state, 'array has more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
210             }
211              
212 526     526   1987 sub _traverse_keyword_minItems { goto \&_assert_non_negative_integer }
213              
214 492     492   939 sub _eval_keyword_minItems ($self, $data, $schema, $state) {
  492         941  
  492         885  
  492         915  
  492         832  
  492         891  
215 492 100       1449 return 1 if not is_type('array', $data);
216 274 100       1311 return 1 if @$data >= $schema->{minItems};
217 128 100       719 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
218             }
219              
220 862     862   1616 sub _traverse_keyword_uniqueItems ($self, $schema, $state) {
  862         1565  
  862         1380  
  862         1451  
  862         1412  
221 862 50       2569 return if not assert_keyword_type($state, $schema, 'boolean');
222 862         11950 return 1;
223             }
224              
225 837     837   1542 sub _eval_keyword_uniqueItems ($self, $data, $schema, $state) {
  837         1821  
  837         1483  
  837         1394  
  837         1376  
  837         1375  
226 837 100       2471 return 1 if not is_type('array', $data);
227 625 100       3139 return 1 if not $schema->{uniqueItems};
228 460 100       5072 return 1 if is_elements_unique($data, my $equal_indices = []);
229 211         1025 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
230             }
231              
232             # Note: no effort is made to check if the 'contains' keyword has been disabled via its vocabulary.
233 92     92   417 sub _traverse_keyword_maxContains { goto \&_assert_non_negative_integer }
234              
235 84     84   157 sub _eval_keyword_maxContains ($self, $data, $schema, $state) {
  84         167  
  84         146  
  84         135  
  84         146  
  84         131  
236 84 100       222 return 1 if not exists $state->{_num_contains};
237 76 50       240 return 1 if not is_type('array', $data);
238              
239             return E($state, 'array contains more than %d matching items', $schema->{maxContains})
240 76 100       363 if $state->{_num_contains} > $schema->{maxContains};
241              
242 44         151 return 1;
243             }
244              
245 124     124   474 sub _traverse_keyword_minContains { goto \&_assert_non_negative_integer }
246              
247 102     102   198 sub _eval_keyword_minContains ($self, $data, $schema, $state) {
  102         188  
  102         194  
  102         166  
  102         201  
  102         159  
248 102 100       280 return 1 if not exists $state->{_num_contains};
249 94 50       256 return 1 if not is_type('array', $data);
250              
251             return E($state, 'array contains fewer than %d matching items', $schema->{minContains})
252 94 100       388 if $state->{_num_contains} < $schema->{minContains};
253              
254 60         215 return 1;
255             }
256              
257 396     396   1748 sub _traverse_keyword_maxProperties { goto \&_assert_non_negative_integer }
258              
259 356     356   712 sub _eval_keyword_maxProperties ($self, $data, $schema, $state) {
  356         762  
  356         662  
  356         577  
  356         581  
  356         606  
260 356 100       969 return 1 if not is_type('object', $data);
261 210 100       1148 return 1 if keys %$data <= $schema->{maxProperties};
262             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
263 102 100       574 $schema->{maxProperties} > 1 ? 'ies' : 'y');
264             }
265              
266 414     414   1554 sub _traverse_keyword_minProperties { goto \&_assert_non_negative_integer }
267              
268 356     356   762 sub _eval_keyword_minProperties ($self, $data, $schema, $state) {
  356         690  
  356         669  
  356         631  
  356         648  
  356         595  
269 356 100       1063 return 1 if not is_type('object', $data);
270 210 100       1055 return 1 if keys %$data >= $schema->{minProperties};
271             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
272 102 100       573 $schema->{minProperties} > 1 ? 'ies' : 'y');
273             }
274              
275 1715     1715   3160 sub _traverse_keyword_required ($self, $schema, $state) {
  1715         3213  
  1715         3001  
  1715         2733  
  1715         2736  
276 1715 50       4476 return if not assert_keyword_type($state, $schema, 'array');
277             return E($state, '"required" element is not a string')
278 1715 50   1913   10307 if any { !is_type('string', $_) } $schema->{required}->@*;
  1913         5112  
279 1715 50       8514 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
280 1715         5232 return 1;
281             }
282              
283 1723     1723   3114 sub _eval_keyword_required ($self, $data, $schema, $state) {
  1723         3237  
  1723         3139  
  1723         2939  
  1723         2911  
  1723         2761  
284 1723 100       4655 return 1 if not is_type('object', $data);
285              
286 1565         6856 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
287 1565 100       6002 return 1 if not @missing;
288 711 100       3679 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
289             }
290              
291 320     320   689 sub _traverse_keyword_dependentRequired ($self, $schema, $state) {
  320         588  
  320         518  
  320         563  
  320         530  
292 320 50       989 return if not assert_keyword_type($state, $schema, 'object');
293              
294 320         734 my $valid = 1;
295 320         1291 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
296             $valid = E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
297 336 50       1002 if not is_type('array', $schema->{dependentRequired}{$property});
298              
299 336         1332 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
300             $valid = E({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
301 350 100       972 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
302             }
303              
304             $valid = E({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
305 336 50       1218 if not is_elements_unique($schema->{dependentRequired}{$property});
306             }
307 320         1421 return $valid;
308             }
309              
310 290     290   586 sub _eval_keyword_dependentRequired ($self, $data, $schema, $state) {
  290         571  
  290         509  
  290         511  
  290         479  
  290         467  
311 290 100       851 return 1 if not is_type('object', $data);
312              
313 185         733 my $valid = 1;
314 185         768 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
315 201 100       597 next if not exists $data->{$property};
316              
317 165 100       997 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
318 85 100       1716 $valid = E({ %$state, _schema_path_suffix => $property },
319             'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
320             }
321             }
322              
323 185 100       800 return 1 if $valid;
324 85         271 return E($state, 'not all dependencies are satisfied');
325             }
326              
327 2595     2595   4956 sub _assert_number ($self, $schema, $state) {
  2595         4749  
  2595         4399  
  2595         4240  
  2595         4098  
328 2595 100       8036 return if not assert_keyword_type($state, $schema, 'number');
329 2587         8463 return 1;
330             }
331              
332 3309     3309   6206 sub _assert_non_negative_integer ($self, $schema, $state) {
  3309         6191  
  3309         5480  
  3309         5230  
  3309         5367  
333 3309 50       9697 return if not assert_keyword_type($state, $schema, 'integer');
334             return E($state, '%s value is not a non-negative integer', $state->{keyword})
335 3309 50       12603 if $schema->{$state->{keyword}} < 0;
336 3309         32854 return 1;
337             }
338              
339             1;
340              
341             __END__
342              
343             =pod
344              
345             =encoding UTF-8
346              
347             =head1 NAME
348              
349             JSON::Schema::Modern::Vocabulary::Validation - Implementation of the JSON Schema Validation vocabulary
350              
351             =head1 VERSION
352              
353             version 0.571
354              
355             =head1 DESCRIPTION
356              
357             =for Pod::Coverage vocabulary evaluation_order keywords
358              
359             =for stopwords metaschema
360              
361             Implementation of the JSON Schema Draft 2020-12 "Validation" vocabulary, indicated in metaschemas
362             with the URI C<https://json-schema.org/draft/2020-12/vocab/validation> and formally specified in
363             L<https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6>.
364              
365             Support is also provided for
366              
367             =over 4
368              
369             =item *
370              
371             the equivalent Draft 2019-09 keywords, indicated in metaschemas with the URI C<https://json-schema.org/draft/2019-09/vocab/validation> and formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02#section-6>.
372              
373             =item *
374              
375             the equivalent Draft 7 keywords that correspond to this vocabulary and are formally specified in L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6>.
376              
377             =back
378              
379             =for stopwords OpenAPI
380              
381             =head1 SUPPORT
382              
383             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
384              
385             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
386              
387             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
388             server|https://open-api.slack.com>, which are also great resources for finding help.
389              
390             =head1 AUTHOR
391              
392             Karen Etheridge <ether@cpan.org>
393              
394             =head1 COPYRIGHT AND LICENCE
395              
396             This software is copyright (c) 2020 by Karen Etheridge.
397              
398             This is free software; you can redistribute it and/or modify it under
399             the same terms as the Perl 5 programming language system itself.
400              
401             =cut