File Coverage

blib/lib/JSON/Schema/Draft201909/Vocabulary/Validation.pm
Criterion Covered Total %
statement 191 192 99.4
branch 129 148 87.1
condition 8 11 72.7
subroutine 58 59 98.3
pod 0 2 0.0
total 386 412 93.6


line stmt bran cond sub pod time code
1 20     20   14057 use strict;
  20         58  
  20         714  
2 20     20   121 use warnings;
  20         51  
  20         1391  
3             package JSON::Schema::Draft201909::Vocabulary::Validation;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Draft 2019-09 Validation vocabulary
6              
7             our $VERSION = '0.028';
8              
9 20     20   454 use 5.016;
  20         81  
10 20     20   127 no if "$]" >= 5.031009, feature => 'indirect';
  20         49  
  20         224  
11 20     20   1011 no if "$]" >= 5.033001, feature => 'multidimensional';
  20         51  
  20         119  
12 20     20   920 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  20         55  
  20         167  
13 20     20   881 use strictures 2;
  20         183  
  20         911  
14 20     20   4103 use List::Util 'any';
  20         55  
  20         1707  
15 20     20   150 use Ref::Util 0.100 'is_plain_arrayref';
  20         410  
  20         1190  
16 20     20   153 use JSON::Schema::Draft201909::Utilities qw(is_type is_equal is_elements_unique E assert_keyword_type assert_pattern);
  20         55  
  20         1625  
17 20     20   148 use Moo;
  20         70  
  20         150  
18 20     20   8943 use namespace::clean;
  20         63  
  20         145  
19              
20             with 'JSON::Schema::Draft201909::Vocabulary';
21              
22 0     0 0 0 sub vocabulary { 'https://json-schema.org/draft/2019-09/vocab/validation' }
23              
24             sub keywords {
25 15954     15954 0 72824 qw(type enum const
26             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
27             maxLength minLength pattern
28             maxItems minItems uniqueItems
29             maxContains minContains
30             maxProperties minProperties required dependentRequired);
31             }
32              
33             sub _traverse_keyword_type {
34 1870     1870   4829 my ($self, $schema, $state) = @_;
35              
36 1870 100       5647 if (is_plain_arrayref($schema->{type})) {
37 82 50       165 abort($state, 'type array is empty') if not @{$schema->{type}};
  82         269  
38 82         170 foreach my $type (@{$schema->{type}}) {
  82         217  
39             return E($state, 'unrecognized type "%s"', $type//'<null>')
40 170 100 50 663   768 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  663   100     1800  
41             }
42 80 50       327 return E($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
43             }
44             else {
45 1788 100       5370 return if not assert_keyword_type($state, $schema, 'string');
46             return E($state, 'unrecognized type "%s"', $schema->{type}//'<null>')
47 1786 100 50 8193   11149 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  8193   50     21401  
48             }
49             }
50              
51             sub _eval_keyword_type {
52 2163     2163   6099 my ($self, $data, $schema, $state) = @_;
53              
54 2163 100       6360 if (is_plain_arrayref($schema->{type})) {
55 431         712 foreach my $type (@{$schema->{type}}) {
  431         987  
56 476 100       1211 return 1 if is_type($type, $data);
57             }
58 29         78 return E($state, 'wrong type (expected one of %s)', join(', ', @{$schema->{type}}));
  29         135  
59             }
60             else {
61 1732 100       5696 return 1 if is_type($schema->{type}, $data);
62 540         2650 return E($state, 'wrong type (expected %s)', $schema->{type});
63             }
64             }
65              
66             sub _traverse_keyword_enum {
67 143     143   344 my ($self, $schema, $state) = @_;
68 143 50       392 return if not assert_keyword_type($state, $schema, 'array');
69 143 50       416 return E($state, '"enum" values are not unique') if not is_elements_unique($schema->{enum});
70             }
71              
72             sub _eval_keyword_enum {
73 128     128   398 my ($self, $data, $schema, $state) = @_;
74              
75 128         241 my @s; my $idx = 0;
  128         223  
76 128 100   234   626 return 1 if any { is_equal($data, $_, $s[$idx++] = {}) } @{$schema->{enum}};
  234         869  
  128         523  
77              
78             return E($state, 'value does not match'
79             .(!(grep $_->{path}, @s) ? ''
80 57 100       516 : ' (differences start '.join(', ', map 'from item #'.$_.' at "'.$s[$_]->{path}.'"', 0..$#s).')'));
81             }
82              
83             sub _eval_keyword_const {
84 336     336   1025 my ($self, $data, $schema, $state) = @_;
85              
86 336 100       1239 return 1 if is_equal($data, $schema->{const}, my $s = {});
87             return E($state, 'value does not match'
88 131 100       727 .($s->{path} ? ' (differences start at "'.$s->{path}.'")' : ''));
89             }
90              
91             sub _traverse_keyword_multipleOf {
92 302     302   712 my ($self, $schema, $state) = @_;
93 302 50       983 return if not assert_keyword_type($state, $schema, 'number');
94 302 50       1104 return E($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
95             }
96              
97             sub _eval_keyword_multipleOf {
98 282     282   771 my ($self, $data, $schema, $state) = @_;
99              
100 282 100       909 return 1 if not is_type('number', $data);
101              
102 215         686 my $quotient = $data / $schema->{multipleOf};
103 215 100 100     1266 return 1 if int($quotient) == $quotient and $quotient !~ /^-?Inf$/i;
104 103         389 return E($state, 'value is not a multiple of %g', $schema->{multipleOf});
105             }
106              
107 234     234   1396 sub _traverse_keyword_maximum { goto \&_assert_number }
108              
109             sub _eval_keyword_maximum {
110 199     199   734 my ($self, $data, $schema, $state) = @_;
111              
112 199 100       808 return 1 if not is_type('number', $data);
113 130 100       625 return 1 if $data <= $schema->{maximum};
114 63         289 return E($state, 'value is larger than %g', $schema->{maximum});
115             }
116              
117 187     187   1036 sub _traverse_keyword_exclusiveMaximum { goto \&_assert_number }
118              
119             sub _eval_keyword_exclusiveMaximum {
120 168     168   600 my ($self, $data, $schema, $state) = @_;
121              
122 168 100       665 return 1 if not is_type('number', $data);
123 101 100       467 return 1 if $data < $schema->{exclusiveMaximum};
124 53         249 return E($state, 'value is equal to or larger than %g', $schema->{exclusiveMaximum});
125             }
126              
127 243     243   1146 sub _traverse_keyword_minimum { goto \&_assert_number }
128              
129             sub _eval_keyword_minimum {
130 231     231   707 my ($self, $data, $schema, $state) = @_;
131              
132 231 100       828 return 1 if not is_type('number', $data);
133 162 100       653 return 1 if $data >= $schema->{minimum};
134 90         341 return E($state, 'value is smaller than %g', $schema->{minimum});
135             }
136              
137 165     165   1100 sub _traverse_keyword_exclusiveMinimum { goto \&_assert_number }
138              
139             sub _eval_keyword_exclusiveMinimum {
140 147     147   530 my ($self, $data, $schema, $state) = @_;
141              
142 147 100       585 return 1 if not is_type('number', $data);
143 80 100       439 return 1 if $data > $schema->{exclusiveMinimum};
144 42         199 return E($state, 'value is equal to or smaller than %g', $schema->{exclusiveMinimum});
145             }
146              
147 214     214   1160 sub _traverse_keyword_maxLength { goto \&_assert_non_negative_integer }
148              
149             sub _eval_keyword_maxLength {
150 186     186   698 my ($self, $data, $schema, $state) = @_;
151              
152 186 100       677 return 1 if not is_type('string', $data);
153 112 100       586 return 1 if length($data) <= $schema->{maxLength};
154 54         251 return E($state, 'length is greater than %d', $schema->{maxLength});
155             }
156              
157 200     200   1054 sub _traverse_keyword_minLength { goto \&_assert_non_negative_integer }
158              
159             sub _eval_keyword_minLength {
160 173     173   625 my ($self, $data, $schema, $state) = @_;
161              
162 173 100       626 return 1 if not is_type('string', $data);
163 100 100       475 return 1 if length($data) >= $schema->{minLength};
164 47         206 return E($state, 'length is less than %d', $schema->{minLength});
165             }
166              
167             sub _traverse_keyword_pattern {
168 313     313   931 my ($self, $schema, $state) = @_;
169 313 50       1148 return if not assert_keyword_type($state, $schema, 'string');
170 313         1304 assert_pattern($state, $schema->{pattern});
171             }
172              
173             sub _eval_keyword_pattern {
174 324     324   1153 my ($self, $data, $schema, $state) = @_;
175              
176 324 100       1035 return 1 if not is_type('string', $data);
177              
178 244 100       2192 return 1 if $data =~ m/$schema->{pattern}/;
179 114         437 return E($state, 'pattern does not match');
180             }
181              
182 174     174   760 sub _traverse_keyword_maxItems { goto \&_assert_non_negative_integer }
183              
184             sub _eval_keyword_maxItems {
185 146     146   412 my ($self, $data, $schema, $state) = @_;
186              
187 146 100       413 return 1 if not is_type('array', $data);
188 86 100       367 return 1 if @$data <= $schema->{maxItems};
189 41 100       206 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
190             }
191              
192 174     174   653 sub _traverse_keyword_minItems { goto \&_assert_non_negative_integer }
193              
194             sub _eval_keyword_minItems {
195 158     158   436 my ($self, $data, $schema, $state) = @_;
196              
197 158 100       445 return 1 if not is_type('array', $data);
198 85 100       364 return 1 if @$data >= $schema->{minItems};
199 41 100       204 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
200             }
201              
202             sub _traverse_keyword_uniqueItems {
203 279     279   748 my ($self, $schema, $state) = @_;
204 279 50       740 return if not assert_keyword_type($state, $schema, 'boolean');
205             }
206              
207             sub _eval_keyword_uniqueItems {
208 268     268   680 my ($self, $data, $schema, $state) = @_;
209              
210 268 100       759 return 1 if not is_type('array', $data);
211 197 100       954 return 1 if not $schema->{uniqueItems};
212 142 100       1472 return 1 if is_elements_unique($data, my $equal_indices = []);
213 62         243 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
214             }
215              
216             # Note: no effort is made to check if the 'contains' keyword has been disabled via its vocabulary.
217 36     36   138 sub _traverse_keyword_maxContains { goto \&_assert_non_negative_integer }
218              
219             sub _eval_keyword_maxContains {
220 32     32   81 my ($self, $data, $schema, $state) = @_;
221              
222 32 100       95 return 1 if not exists $state->{_num_contains};
223 28 50       88 return 1 if not is_type('array', $data);
224              
225             return E($state, 'contains too many matching items')
226 28 100       99 if $state->{_num_contains} > $schema->{maxContains};
227              
228 16         44 return 1;
229             }
230              
231 52     52   173 sub _traverse_keyword_minContains { goto \&_assert_non_negative_integer }
232              
233             sub _eval_keyword_minContains {
234 42     42   107 my ($self, $data, $schema, $state) = @_;
235              
236 42 100       133 return 1 if not exists $state->{_num_contains};
237 38 50       101 return 1 if not is_type('array', $data);
238              
239             return E($state, 'contains too few matching items')
240 38 100       137 if $state->{_num_contains} < $schema->{minContains};
241              
242 23         60 return 1;
243             }
244              
245 128     128   575 sub _traverse_keyword_maxProperties { goto \&_assert_non_negative_integer }
246              
247             sub _eval_keyword_maxProperties {
248 111     111   365 my ($self, $data, $schema, $state) = @_;
249              
250 111 100       387 return 1 if not is_type('object', $data);
251 64 100       356 return 1 if keys %$data <= $schema->{maxProperties};
252             return E($state, 'more than %d propert%s', $schema->{maxProperties},
253 31 100       188 $schema->{maxProperties} > 1 ? 'ies' : 'y');
254             }
255              
256 132     132   540 sub _traverse_keyword_minProperties { goto \&_assert_non_negative_integer }
257              
258             sub _eval_keyword_minProperties {
259 111     111   361 my ($self, $data, $schema, $state) = @_;
260              
261 111 100       413 return 1 if not is_type('object', $data);
262 64 100       301 return 1 if keys %$data >= $schema->{minProperties};
263             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
264 31 100       215 $schema->{minProperties} > 1 ? 'ies' : 'y');
265             }
266              
267             sub _traverse_keyword_required {
268 357     357   926 my ($self, $schema, $state) = @_;
269              
270 357 50       1061 return if not assert_keyword_type($state, $schema, 'array');
271             return E($state, '"required" element is not a string')
272 357 50   395   1543 if any { !is_type('string', $_) } @{$schema->{required}};
  395         1101  
  357         1372  
273 357 50       2020 return E($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
274             }
275              
276             sub _eval_keyword_required {
277 352     352   1022 my ($self, $data, $schema, $state) = @_;
278              
279 352 100       1016 return 1 if not is_type('object', $data);
280              
281 305         651 my @missing = grep !exists $data->{$_}, @{$schema->{required}};
  305         1229  
282 305 100       1251 return 1 if not @missing;
283 88 100       530 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
284             }
285              
286             sub _traverse_keyword_dependentRequired {
287 152     152   400 my ($self, $schema, $state) = @_;
288              
289 152 50       463 return if not assert_keyword_type($state, $schema, 'object');
290              
291 152         395 foreach my $property (sort keys %{$schema->{dependentRequired}}) {
  152         734  
292             E({ %$state, _schema_path_suffix => $property }, 'dependentRequired value is not an array'), next
293 160 50       502 if not is_type('array', $schema->{dependentRequired}{$property});
294              
295 160         354 foreach my $index (0..$#{$schema->{dependentRequired}{$property}}) {
  160         698  
296             E({ %$state, _schema_path_suffix => $property }, 'element #%d is not a string', $index)
297 166 50       553 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
298             }
299              
300             E({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
301 160 50       651 if not is_elements_unique($schema->{dependentRequired}{$property});
302             }
303             }
304              
305             sub _eval_keyword_dependentRequired {
306 135     135   457 my ($self, $data, $schema, $state) = @_;
307              
308 135 100       466 return 1 if not is_type('object', $data);
309              
310 86         219 my $valid = 1;
311 86         191 foreach my $property (sort keys %{$schema->{dependentRequired}}) {
  86         438  
312 94 100       336 next if not exists $data->{$property};
313              
314 76 100       141 if (my @missing = grep !exists($data->{$_}), @{$schema->{dependentRequired}{$property}}) {
  76         497  
315 39 100       664 $valid = E({ %$state, _schema_path_suffix => $property },
316             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
317             }
318             }
319              
320 86 100       374 return 1 if $valid;
321 39         137 return E($state, 'not all dependencies are satisfied');
322             }
323              
324             sub _assert_number {
325 829     829   2613 my ($self, $schema, $state) = @_;
326 829 100       3309 return if not assert_keyword_type($state, $schema, 'number');
327             }
328              
329             sub _assert_non_negative_integer {
330 1110     1110   3128 my ($self, $schema, $state) = @_;
331 1110 50       3520 return if not assert_keyword_type($state, $schema, 'integer');
332             return E($state, '%s value is not a non-negative integer', $state->{keyword})
333 1110 50       5080 if $schema->{$state->{keyword}} < 0;
334             }
335              
336             1;
337              
338             __END__
339              
340             =pod
341              
342             =encoding UTF-8
343              
344             =head1 NAME
345              
346             JSON::Schema::Draft201909::Vocabulary::Validation - Implementation of the JSON Schema Draft 2019-09 Validation vocabulary
347              
348             =head1 VERSION
349              
350             version 0.028
351              
352             =head1 DESCRIPTION
353              
354             =for Pod::Coverage vocabulary keywords
355              
356             =for stopwords metaschema
357              
358             Implementation of the JSON Schema Draft 2019-09 "Validation" vocabulary, indicated in metaschemas
359             with the URI C<https://json-schema.org/draft/2019-09/vocab/validation> and formally specified in
360             L<https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.6>.
361              
362             =head1 SUPPORT
363              
364             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Draft201909/issues>.
365              
366             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
367              
368             =head1 AUTHOR
369              
370             Karen Etheridge <ether@cpan.org>
371              
372             =head1 COPYRIGHT AND LICENCE
373              
374             This software is copyright (c) 2020 by Karen Etheridge.
375              
376             This is free software; you can redistribute it and/or modify it under
377             the same terms as the Perl 5 programming language system itself.
378              
379             =cut