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   18298 use strict;
  31         87  
  31         1086  
2 31     31   179 use warnings;
  31         78  
  31         1790  
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.572';
8              
9 31     31   594 use 5.020;
  31         132  
10 31     31   174 use Moo;
  31         76  
  31         194  
11 31     31   11591 use strictures 2;
  31         245  
  31         1191  
12 31     31   6030 use stable 0.031 'postderef';
  31         572  
  31         193  
13 31     31   4682 use experimental 'signatures';
  31         94  
  31         172  
14 31     31   2646 use if "$]" >= 5.022, experimental => 're_strict';
  31         101  
  31         329  
15 31     31   2871 no if "$]" >= 5.031009, feature => 'indirect';
  31         105  
  31         253  
16 31     31   1595 no if "$]" >= 5.033001, feature => 'multidimensional';
  31         88  
  31         221  
17 31     31   1607 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  31         102  
  31         218  
18 31     31   1250 use List::Util 'any';
  31         107  
  31         2664  
19 31     31   255 use Ref::Util 0.100 'is_plain_arrayref';
  31         587  
  31         1654  
20 31     31   213 use Scalar::Util 'looks_like_number';
  31         84  
  31         2194  
21 31     31   217 use if "$]" >= 5.022, POSIX => 'isinf';
  31         74  
  31         297  
22 31     31   57651 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         81  
  31         2776  
23 31     31   33399 use Math::BigFloat;
  31         1691197  
  31         221  
24 31     31   723992 use namespace::clean;
  31         94  
  31         364  
25              
26             with 'JSON::Schema::Modern::Vocabulary';
27              
28             sub vocabulary {
29 160     160 0 891 '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 37 sub evaluation_order { 1 }
34              
35 92     92 0 249 sub keywords ($self, $spec_version) {
  92         215  
  92         218  
  92         179  
36             return (
37 92 100       6912 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   11050 sub _traverse_keyword_type ($self, $schema, $state) {
  6021         10246  
  6021         9891  
  6021         9224  
  6021         9317  
48 6021 100       15951 if (is_plain_arrayref($schema->{type})) {
49 279 50       926 return E($state, 'type array is empty') if not $schema->{type}->@*;
50 279         756 foreach my $type ($schema->{type}->@*) {
51             return E($state, 'unrecognized type "%s"', $type//'<null>')
52 576 100 50 2123   2515 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  2123   100     5604  
53             }
54 277 50       1134 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
55             }
56             else {
57 5742 100       16673 return if not assert_keyword_type($state, $schema, 'string');
58             return E($state, 'unrecognized type "%s"', $schema->{type}//'<null>')
59 5740 100 50 26691   36666 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  26691   50     61270  
60             }
61 6013         27877 return 1;
62             }
63              
64 7006     7006   12171 sub _eval_keyword_type ($self, $data, $schema, $state) {
  7006         13728  
  7006         12141  
  7006         10887  
  7006         11667  
  7006         10989  
65 7006         20289 my $type = get_type($data);
66 7006 100       27378 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   15778 or ($_ eq 'boolean' and $state->{scalarref_booleans} and $type eq 'reference to SCALAR')
      100        
      66        
      66        
      100        
      100        
      66        
      66        
      33        
72 1522 100       10707 } $schema->{type}->@*;
73 113         1020 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     37672 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         5520 return E($state, 'got %s, not %s', $type, $schema->{type});
81             }
82             }
83              
84 499     499   885 sub _traverse_keyword_enum ($self, $schema, $state) {
  499         913  
  499         841  
  499         794  
  499         755  
85 499 50       1375 return if not assert_keyword_type($state, $schema, 'array');
86 499         1407 return 1;
87             }
88              
89 456     456   844 sub _eval_keyword_enum ($self, $data, $schema, $state) {
  456         907  
  456         843  
  456         802  
  456         1017  
  456         710  
90 456         804 my @s; my $idx = 0;
  456         753  
91 456         1519 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
92 456 100   1117   3439 return 1 if any { is_equal($data, $_, $s[$idx++] = {%s}) } $schema->{enum}->@*;
  1117         12803  
93             return E($state, 'value does not match'
94             .(!(grep $_->{path}, @s) ? ''
95 188 100       1785 : ' (differences start '.join(', ', map 'from item #'.$_.' at "'.$s[$_]->{path}.'"', 0..$#s).')'));
96             }
97              
98 1241     1241   3389 sub _traverse_keyword_const { 1 }
99              
100 1127     1127   2302 sub _eval_keyword_const ($self, $data, $schema, $state) {
  1127         2104  
  1127         2045  
  1127         1923  
  1127         1955  
  1127         1747  
101 1127         3309 my %s = ( scalarref_booleans => $state->{scalarref_booleans} );
102 1127 100       5193 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       6237 .($s->{path} ? ' (differences start at "'.$s->{path}.'")' : ''));
105             }
106              
107 903     903   1596 sub _traverse_keyword_multipleOf ($self, $schema, $state) {
  903         1729  
  903         1502  
  903         1516  
  903         1431  
108 903 100       2561 return if not assert_keyword_type($state, $schema, 'number');
109 901 50       2822 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
110 901         13498 return 1;
111             }
112              
113 906     906   1735 sub _eval_keyword_multipleOf ($self, $data, $schema, $state) {
  906         1666  
  906         1576  
  906         1609  
  906         1522  
  906         1498  
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     2883 and do { $data = 0+$data; 1 });
  2   66     8  
  2   33     11  
      100        
117              
118             # if either value is a float, use the bignum library for the calculation for an accurate remainder
119 700 100 100     4441 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       364 $data = ref($data) =~ /^Math::Big(?:Int|Float)$/ ? $data->copy : Math::BigFloat->new($data);
123 52 100       3407 my $divisor = ref($schema->{multipleOf}) =~ /^Math::Big(?:Int|Float)$/ ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
124 52         1344 my ($quotient, $remainder) = $data->bdiv($divisor);
125 52 50       55879 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
126 52 100       556 return 1 if $remainder == 0;
127             }
128             else {
129 648         1868 my $quotient = $data / $schema->{multipleOf};
130 648 50       4280 return E($state, 'overflow while calculating quotient of integers')
    50          
131             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
132 648 100       2729 return 1 if int($quotient) == $quotient;
133             }
134              
135 326         5989 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
136             }
137              
138 769     769   3325 sub _traverse_keyword_maximum { goto \&_assert_number }
139              
140 691     691   1461 sub _eval_keyword_maximum ($self, $data, $schema, $state) {
  691         1334  
  691         1329  
  691         1260  
  691         1188  
  691         1171  
141             return 1 if not is_type('number', $data)
142 691 50 66     2165 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
143 480 100       2237 return 1 if 0+$data <= $schema->{maximum};
144 214         8547 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
145             }
146              
147 550     550   2384 sub _traverse_keyword_exclusiveMaximum { goto \&_assert_number }
148              
149 514     514   1053 sub _eval_keyword_exclusiveMaximum ($self, $data, $schema, $state) {
  514         1324  
  514         981  
  514         919  
  514         937  
  514         814  
150             return 1 if not is_type('number', $data)
151 514 50 66     1528 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
152 309 100       1443 return 1 if 0+$data < $schema->{exclusiveMaximum};
153 163         9666 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
154             }
155              
156 787     787   3304 sub _traverse_keyword_minimum { goto \&_assert_number }
157              
158 767     767   1519 sub _eval_keyword_minimum ($self, $data, $schema, $state) {
  767         1477  
  767         1355  
  767         1323  
  767         1310  
  767         1231  
159             return 1 if not is_type('number', $data)
160 767 50 66     2375 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
161 547 100       2529 return 1 if 0+$data >= $schema->{minimum};
162 266         17113 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
163             }
164              
165 489     489   2087 sub _traverse_keyword_exclusiveMinimum { goto \&_assert_number }
166              
167 453     453   911 sub _eval_keyword_exclusiveMinimum ($self, $data, $schema, $state) {
  453         820  
  453         824  
  453         846  
  453         737  
  453         750  
168             return 1 if not is_type('number', $data)
169 453 50 66     1392 and not ($state->{stringy_numbers} and is_type('string', $data) and looks_like_number($data));
      66        
      100        
170 248 100       1129 return 1 if 0+$data > $schema->{exclusiveMinimum};
171 132         8595 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
172             }
173              
174 642     642   2647 sub _traverse_keyword_maxLength { goto \&_assert_non_negative_integer }
175              
176 585     585   1185 sub _eval_keyword_maxLength ($self, $data, $schema, $state) {
  585         1171  
  585         1136  
  585         1004  
  585         1112  
  585         905  
177 585 100       1743 return 1 if not is_type('string', $data);
178 360 100       1699 return 1 if length($data) <= $schema->{maxLength};
179 170         710 return E($state, 'length is greater than %d', $schema->{maxLength});
180             }
181              
182 603     603   2441 sub _traverse_keyword_minLength { goto \&_assert_non_negative_integer }
183              
184 553     553   1055 sub _eval_keyword_minLength ($self, $data, $schema, $state) {
  553         1034  
  553         1051  
  553         941  
  553         1108  
  553         971  
185              
186 553 100       1595 return 1 if not is_type('string', $data);
187 327 100       1555 return 1 if length($data) >= $schema->{minLength};
188 154         576 return E($state, 'length is less than %d', $schema->{minLength});
189             }
190              
191 986     986   1903 sub _traverse_keyword_pattern ($self, $schema, $state) {
  986         1877  
  986         1725  
  986         1653  
  986         1583  
192             return if not assert_keyword_type($state, $schema, 'string')
193 986 100 66     2899 or not assert_pattern($state, $schema->{pattern});
194 985         3283 return 1;
195             }
196              
197 1028     1028   2197 sub _eval_keyword_pattern ($self, $data, $schema, $state) {
  1028         1959  
  1028         2052  
  1028         1781  
  1028         1725  
  1028         1834  
198 1028 100       3169 return 1 if not is_type('string', $data);
199              
200 785 100       8810 return 1 if $data =~ m/(?:$schema->{pattern})/;
201 368         1498 return E($state, 'pattern does not match');
202             }
203              
204 512     512   2080 sub _traverse_keyword_maxItems { goto \&_assert_non_negative_integer }
205              
206 441     441   810 sub _eval_keyword_maxItems ($self, $data, $schema, $state) {
  441         827  
  441         881  
  441         770  
  441         768  
  441         770  
207 441 100       1392 return 1 if not is_type('array', $data);
208 264 100       1226 return 1 if @$data <= $schema->{maxItems};
209 126 100       682 return E($state, 'array has more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
210             }
211              
212 526     526   2067 sub _traverse_keyword_minItems { goto \&_assert_non_negative_integer }
213              
214 492     492   1045 sub _eval_keyword_minItems ($self, $data, $schema, $state) {
  492         942  
  492         930  
  492         882  
  492         884  
  492         800  
215 492 100       1445 return 1 if not is_type('array', $data);
216 274 100       1184 return 1 if @$data >= $schema->{minItems};
217 128 100       660 return E($state, 'array has fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
218             }
219              
220 862     862   1545 sub _traverse_keyword_uniqueItems ($self, $schema, $state) {
  862         1727  
  862         1489  
  862         1412  
  862         1371  
221 862 50       2635 return if not assert_keyword_type($state, $schema, 'boolean');
222 862         11858 return 1;
223             }
224              
225 837     837   1538 sub _eval_keyword_uniqueItems ($self, $data, $schema, $state) {
  837         1660  
  837         1567  
  837         1733  
  837         1365  
  837         1374  
226 837 100       2524 return 1 if not is_type('array', $data);
227 625 100       3305 return 1 if not $schema->{uniqueItems};
228 460 100       4960 return 1 if is_elements_unique($data, my $equal_indices = []);
229 211         931 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   400 sub _traverse_keyword_maxContains { goto \&_assert_non_negative_integer }
234              
235 84     84   178 sub _eval_keyword_maxContains ($self, $data, $schema, $state) {
  84         137  
  84         178  
  84         147  
  84         157  
  84         123  
236 84 100       256 return 1 if not exists $state->{_num_contains};
237 76 50       209 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       339 if $state->{_num_contains} > $schema->{maxContains};
241              
242 44         128 return 1;
243             }
244              
245 124     124   433 sub _traverse_keyword_minContains { goto \&_assert_non_negative_integer }
246              
247 102     102   174 sub _eval_keyword_minContains ($self, $data, $schema, $state) {
  102         179  
  102         176  
  102         190  
  102         181  
  102         167  
248 102 100       285 return 1 if not exists $state->{_num_contains};
249 94 50       250 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       407 if $state->{_num_contains} < $schema->{minContains};
253              
254 60         174 return 1;
255             }
256              
257 396     396   1416 sub _traverse_keyword_maxProperties { goto \&_assert_non_negative_integer }
258              
259 356     356   696 sub _eval_keyword_maxProperties ($self, $data, $schema, $state) {
  356         675  
  356         705  
  356         628  
  356         600  
  356         608  
260 356 100       1052 return 1 if not is_type('object', $data);
261 210 100       996 return 1 if keys %$data <= $schema->{maxProperties};
262             return E($state, 'object has more than %d propert%s', $schema->{maxProperties},
263 102 100       561 $schema->{maxProperties} > 1 ? 'ies' : 'y');
264             }
265              
266 414     414   1454 sub _traverse_keyword_minProperties { goto \&_assert_non_negative_integer }
267              
268 356     356   725 sub _eval_keyword_minProperties ($self, $data, $schema, $state) {
  356         645  
  356         686  
  356         639  
  356         652  
  356         640  
269 356 100       1002 return 1 if not is_type('object', $data);
270 210 100       995 return 1 if keys %$data >= $schema->{minProperties};
271             return E($state, 'object has fewer than %d propert%s', $schema->{minProperties},
272 102 100       538 $schema->{minProperties} > 1 ? 'ies' : 'y');
273             }
274              
275 1715     1715   3285 sub _traverse_keyword_required ($self, $schema, $state) {
  1715         3146  
  1715         2901  
  1715         2751  
  1715         2873  
276 1715 50       4566 return if not assert_keyword_type($state, $schema, 'array');
277             return E($state, '"required" element is not a string')
278 1715 50   1913   11074 if any { !is_type('string', $_) } $schema->{required}->@*;
  1913         5518  
279 1715 50       8529 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
280 1715         5025 return 1;
281             }
282              
283 1723     1723   3243 sub _eval_keyword_required ($self, $data, $schema, $state) {
  1723         3146  
  1723         2986  
  1723         3046  
  1723         2889  
  1723         2736  
284 1723 100       4862 return 1 if not is_type('object', $data);
285              
286 1565         6995 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
287 1565 100       5565 return 1 if not @missing;
288 711 100       3815 return E($state, 'object is missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
289             }
290              
291 320     320   595 sub _traverse_keyword_dependentRequired ($self, $schema, $state) {
  320         619  
  320         559  
  320         552  
  320         573  
292 320 50       1163 return if not assert_keyword_type($state, $schema, 'object');
293              
294 320         695 my $valid = 1;
295 320         1399 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
296             $valid = E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
297 336 50       1073 if not is_type('array', $schema->{dependentRequired}{$property});
298              
299 336         1362 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       1019 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       1270 if not is_elements_unique($schema->{dependentRequired}{$property});
306             }
307 320         1052 return $valid;
308             }
309              
310 290     290   550 sub _eval_keyword_dependentRequired ($self, $data, $schema, $state) {
  290         559  
  290         517  
  290         540  
  290         515  
  290         537  
311 290 100       842 return 1 if not is_type('object', $data);
312              
313 185         441 my $valid = 1;
314 185         772 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
315 201 100       666 next if not exists $data->{$property};
316              
317 165 100       969 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
318 85 100       1466 $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       752 return 1 if $valid;
324 85         279 return E($state, 'not all dependencies are satisfied');
325             }
326              
327 2595     2595   4991 sub _assert_number ($self, $schema, $state) {
  2595         4847  
  2595         4417  
  2595         4387  
  2595         4104  
328 2595 100       7842 return if not assert_keyword_type($state, $schema, 'number');
329 2587         8577 return 1;
330             }
331              
332 3309     3309   6589 sub _assert_non_negative_integer ($self, $schema, $state) {
  3309         6203  
  3309         5542  
  3309         5368  
  3309         5205  
333 3309 50       9680 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       11822 if $schema->{$state->{keyword}} < 0;
336 3309         31779 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.572
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