File Coverage

blib/lib/JSON/Schema/Draft201909/Vocabulary/Applicator.pm
Criterion Covered Total %
statement 376 377 99.7
branch 181 186 97.3
condition 40 42 95.2
subroutine 50 51 98.0
pod 0 2 0.0
total 647 658 98.3


line stmt bran cond sub pod time code
1 20     20   27866 use strict;
  20         51  
  20         720  
2 20     20   117 use warnings;
  20         49  
  20         1198  
3             package JSON::Schema::Draft201909::Vocabulary::Applicator;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Draft 2019-09 Applicator vocabulary
6              
7             our $VERSION = '0.028';
8              
9 20     20   404 use 5.016;
  20         78  
10 20     20   121 no if "$]" >= 5.031009, feature => 'indirect';
  20         47  
  20         189  
11 20     20   1318 no if "$]" >= 5.033001, feature => 'multidimensional';
  20         58  
  20         146  
12 20     20   990 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  20         58  
  20         129  
13 20     20   899 use strictures 2;
  20         153  
  20         831  
14 20     20   4206 use List::Util 1.45 qw(any uniqstr max);
  20         575  
  20         1839  
15 20     20   138 use Ref::Util 0.100 'is_plain_arrayref';
  20         412  
  20         1505  
16 20     20   162 use JSON::Schema::Draft201909::Utilities qw(is_type jsonp local_annotations E A abort assert_keyword_type assert_pattern true);
  20         82  
  20         2081  
17 20     20   158 use Moo;
  20         42  
  20         137  
18 20     20   9146 use namespace::clean;
  20         48  
  20         222  
19              
20             with 'JSON::Schema::Draft201909::Vocabulary';
21              
22 0     0 0 0 sub vocabulary { 'https://json-schema.org/draft/2019-09/vocab/applicator' }
23              
24             # the keyword order is arbitrary, except:
25             # - if must be evaluated before then, else
26             # - items must be evaluated before additionalItems
27             # - in-place applicators (allOf, anyOf, oneOf, not, if/then/else, dependentSchemas) and items,
28             # additionalItems must be evaluated before unevaluatedItems
29             # - properties and patternProperties must be evaluated before additionalProperties
30             # - in-place applicators and properties, patternProperties, additionalProperties must be evaluated
31             # before unevaluatedProperties
32             # - contains must be evaluated before maxContains, minContains (in the Validator vocabulary)
33             sub keywords {
34 16658     16658 0 72650 qw(allOf anyOf oneOf not if then else dependentSchemas
35             items additionalItems contains
36             properties patternProperties additionalProperties propertyNames
37             unevaluatedItems unevaluatedProperties);
38             }
39              
40 200     200   791 sub _traverse_keyword_allOf { shift->traverse_array_schemas(@_) }
41              
42             sub _eval_keyword_allOf {
43 251     251   4355 my ($self, $data, $schema, $state) = @_;
44              
45 251         476 my @invalid;
46 251         454 my @orig_annotations = @{$state->{annotations}};
  251         622  
47 251         473 my @new_annotations;
48 251         485 foreach my $idx (0 .. $#{$schema->{allOf}}) {
  251         894  
49 599         1172 my @annotations = @orig_annotations;
50 599 100       8321 if ($self->eval($data, $schema->{allOf}[$idx], +{ %$state,
51             schema_path => $state->{schema_path}.'/allOf/'.$idx, annotations => \@annotations })) {
52 460         3361 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
53             }
54             else {
55 117         356 push @invalid, $idx;
56 117 100       669 last if $state->{short_circuit};
57             }
58             }
59              
60 229 100       887 if (@invalid == 0) {
61 125         233 push @{$state->{annotations}}, @new_annotations;
  125         347  
62 125         475 return 1;
63             }
64              
65 104         264 my $pl = @invalid > 1;
66 104 100       646 return E($state, 'subschema%s %s %s not valid', $pl?'s':'', join(', ', @invalid), $pl?'are':'is');
    100          
67             }
68              
69 269     269   1141 sub _traverse_keyword_anyOf { shift->traverse_array_schemas(@_) }
70              
71             sub _eval_keyword_anyOf {
72 339     339   1045 my ($self, $data, $schema, $state) = @_;
73              
74 339         699 my $valid = 0;
75 339         645 my @errors;
76 339         728 foreach my $idx (0 .. $#{$schema->{anyOf}}) {
  339         1293  
77             next if not $self->eval($data, $schema->{anyOf}[$idx],
78 614 100       8898 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/anyOf/'.$idx });
79 225         1281 ++$valid;
80 225 100       827 last if $state->{short_circuit};
81             }
82              
83 336 100       1523 return 1 if $valid;
84 124         264 push @{$state->{errors}}, @errors;
  124         454  
85 124         423 return E($state, 'no subschemas are valid');
86             }
87              
88 94     94   323 sub _traverse_keyword_oneOf { shift->traverse_array_schemas(@_) }
89              
90             sub _eval_keyword_oneOf {
91 86     86   10308 my ($self, $data, $schema, $state) = @_;
92              
93 86         159 my (@valid, @errors);
94 86         160 my @orig_annotations = @{$state->{annotations}};
  86         189  
95 86         150 my @new_annotations;
96 86         155 foreach my $idx (0 .. $#{$schema->{oneOf}}) {
  86         299  
97 158         321 my @annotations = @orig_annotations;
98             next if not $self->eval($data, $schema->{oneOf}[$idx],
99             +{ %$state, errors => \@errors, annotations => \@annotations,
100 158 100       2012 schema_path => $state->{schema_path}.'/oneOf/'.$idx });
101 86         694 push @valid, $idx;
102 86         247 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
103 86 100 100     382 last if @valid > 1 and $state->{short_circuit};
104             }
105              
106 86 100       266 if (@valid == 1) {
107 45         97 push @{$state->{annotations}}, @new_annotations;
  45         120  
108 45         224 return 1;
109             }
110 41 100       130 if (not @valid) {
111 21         39 push @{$state->{errors}}, @errors;
  21         208  
112 21         104 return E($state, 'no subschemas are valid');
113             }
114             else {
115 20         110 return E($state, 'multiple subschemas are valid: '.join(', ', @valid));
116             }
117             }
118              
119 62     62   301 sub _traverse_keyword_not { shift->traverse_subschema(@_) }
120              
121             sub _eval_keyword_not {
122 59     59   6818 my ($self, $data, $schema, $state) = @_;
123              
124             return 1 if not $self->eval($data, $schema->{not},
125             +{ %$state, schema_path => $state->{schema_path}.'/not',
126             short_circuit => $state->{short_circuit} || !$state->{collect_annotations},
127 59 100 100     507 errors => [], annotations => [ @{$state->{annotations}} ] });
  59         630  
128              
129 41         612 return E($state, 'subschema is valid');
130             }
131              
132 84     84   337 sub _traverse_keyword_if { shift->traverse_subschema(@_) }
133 78     78   271 sub _traverse_keyword_then { shift->traverse_subschema(@_) }
134 72     72   306 sub _traverse_keyword_else { shift->traverse_subschema(@_) }
135              
136             sub _eval_keyword_if {
137 80     80   266 my ($self, $data, $schema, $state) = @_;
138              
139             return 1 if not exists $schema->{then} and not exists $schema->{else}
140 80 50 100     317 and not $state->{collect_annotations};
      66        
141             my $keyword = $self->eval($data, $schema->{if},
142             +{ %$state, schema_path => $state->{schema_path}.'/if',
143             short_circuit => $state->{short_circuit} || !$state->{collect_annotations},
144 72 100 100     1140 errors => [],
145             })
146             ? 'then' : 'else';
147              
148 72 100       651 return 1 if not exists $schema->{$keyword};
149             return 1 if $self->eval($data, $schema->{$keyword},
150 66 100       879 +{ %$state, schema_path => $state->{schema_path}.'/'.$keyword });
151 26         350 return E({ %$state, keyword => $keyword }, 'subschema is not valid');
152             }
153              
154 142     142   839 sub _traverse_keyword_dependentSchemas { shift->traverse_object_schemas(@_) }
155              
156             sub _eval_keyword_dependentSchemas {
157 142     142   462 my ($self, $data, $schema, $state) = @_;
158              
159 142 100       521 return 1 if not is_type('object', $data);
160              
161 86         202 my $valid = 1;
162 86         181 my @orig_annotations = @{$state->{annotations}};
  86         230  
163 86         189 my @new_annotations;
164 86         178 foreach my $property (sort keys %{$schema->{dependentSchemas}}) {
  86         398  
165 99 100       352 next if not exists $data->{$property};
166              
167 53         140 my @annotations = @orig_annotations;
168 53 100       470 if ($self->eval($data, $schema->{dependentSchemas}{$property},
169             +{ %$state, annotations => \@annotations,
170             schema_path => jsonp($state->{schema_path}, 'dependentSchemas', $property) })) {
171 9         89 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
172 9         51 next;
173             }
174              
175 44         221 $valid = 0;
176 44 100       187 last if $state->{short_circuit};
177             }
178              
179 86 100       346 return E($state, 'not all dependencies are satisfied') if not $valid;
180 42         104 push @{$state->{annotations}}, @new_annotations;
  42         120  
181 42         178 return 1;
182             }
183              
184             sub _traverse_keyword_items {
185 500     500   1240 my ($self, $schema, $state) = @_;
186              
187 500 100       2185 return $self->traverse_array_schemas($schema, $state) if is_plain_arrayref($schema->{items});
188 105         430 $self->traverse_subschema($schema, $state);
189             }
190              
191             sub _eval_keyword_items {
192 558     558   1483 my ($self, $data, $schema, $state) = @_;
193              
194 558 100       2642 goto \&_eval_keyword__items_array_schemas if is_plain_arrayref($schema->{items});
195              
196 152   50     1054 $state->{_last_items_index} //= -1;
197 152         767 goto \&_eval_keyword__items_schema;
198             }
199              
200 84     84   311 sub _traverse_keyword_additionalItems { shift->traverse_subschema(@_) }
201              
202             sub _eval_keyword_additionalItems {
203 102     102   318 my ($self, $data, $schema, $state) = @_;
204              
205 102 100       267 return 1 if not exists $state->{_last_items_index};
206 92         402 goto \&_eval_keyword__items_schema;
207             }
208              
209             # array-based items
210             sub _eval_keyword__items_array_schemas {
211 406     406   1073 my ($self, $data, $schema, $state) = @_;
212              
213 406 100       1214 return 1 if not is_type('array', $data);
214              
215 331         676 my @orig_annotations = @{$state->{annotations}};
  331         819  
216 331         571 my @new_annotations;
217 331         594 my $valid = 1;
218              
219 331         646 foreach my $idx (0 .. $#{$data}) {
  331         1130  
220 590 100       1562 last if $idx > $#{$schema->{$state->{keyword}}};
  590         1931  
221 479         1409 $state->{_last_items_index} = $idx;
222              
223 479         1020 my @annotations = @orig_annotations;
224 479 100       1864 if (is_type('boolean', $schema->{$state->{keyword}}[$idx])) {
    100          
225 123 100       2239 next if $schema->{$state->{keyword}}[$idx];
226 39         837 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx,
227             _schema_path_suffix => $idx }, 'item not permitted');
228             }
229             elsif ($self->eval($data->[$idx], $schema->{$state->{keyword}}[$idx],
230             +{ %$state, annotations => \@annotations,
231             data_path => $state->{data_path}.'/'.$idx,
232             schema_path => $state->{schema_path}.'/'.$state->{keyword}.'/'.$idx })) {
233 332         845 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
234 332         1941 next;
235             }
236              
237 63         385 $valid = 0;
238 63 100 100     405 last if $state->{short_circuit} and not exists $schema->{additionalItems};
239             }
240              
241 331 100       1207 return E($state, 'not all items are valid') if not $valid;
242 269         507 push @{$state->{annotations}}, @new_annotations;
  269         606  
243 269         998 return A($state, $state->{_last_items_index});
244             }
245              
246             # schema-based items and additionalItems
247             sub _eval_keyword__items_schema {
248 244     244   682 my ($self, $data, $schema, $state) = @_;
249              
250 244 100       900 return 1 if not is_type('array', $data);
251 215 100       498 return 1 if $state->{_last_items_index} == $#{$data};
  215         676  
252              
253 148         296 my @orig_annotations = @{$state->{annotations}};
  148         369  
254 148         273 my @new_annotations;
255 148         293 my $valid = 1;
256              
257 148         338 foreach my $idx ($state->{_last_items_index}+1 .. $#{$data}) {
  148         470  
258 231 100       827 if (is_type('boolean', $schema->{$state->{keyword}})) {
259 28 100       493 next if $schema->{$state->{keyword}};
260             $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
261 16 100       477 '%sitem not permitted', $state->{keyword} eq 'additionalItems' ? 'additional ' : '');
262             }
263             else {
264 203         1569 my @annotations = @orig_annotations;
265 203 100       3132 if ($self->eval($data->[$idx], $schema->{$state->{keyword}},
266             +{ %$state, annotations => \@annotations,
267             data_path => $state->{data_path}.'/'.$idx,
268             schema_path => $state->{schema_path}.'/'.$state->{keyword} })) {
269 136         355 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
270 136         791 next;
271             }
272              
273 62         331 $valid = 0;
274             }
275 78 100       352 last if $state->{short_circuit};
276             }
277              
278 143         324 $state->{_last_items_index} = $#{$data};
  143         435  
279              
280             return E($state, 'subschema is not valid against all %sitems',
281 143 100       604 $state->{keyword} eq 'additionalItems' ? 'additional ' : '') if not $valid;
    100          
282 76         134 push @{$state->{annotations}}, @new_annotations;
  76         192  
283 76         245 return A($state, true);
284             }
285              
286             sub _traverse_keyword_unevaluatedItems {
287 230     230   642 my ($self, $schema, $state) = @_;
288              
289 230         949 $self->traverse_subschema($schema, $state);
290              
291             # remember that annotations need to be collected in order to evaluate this keyword
292 230         1120 $state->{configs}{collect_annotations} = 1;
293             }
294              
295             sub _eval_keyword_unevaluatedItems {
296 225     225   655 my ($self, $data, $schema, $state) = @_;
297              
298             abort($state, 'EXCEPTION: "unevaluatedItems" keyword present, but annotation collection is disabled')
299 225 100       690 if not $state->{collect_annotations};
300              
301             abort($state, 'EXCEPTION: "unevaluatedItems" keyword present, but short_circuit is enabled: results unreliable')
302 222 100       869 if $state->{short_circuit};
303              
304 118 100       413 return 1 if not is_type('array', $data);
305              
306 82         320 my @annotations = local_annotations($state);
307 82         272 my @items_annotations = grep $_->keyword eq 'items', @annotations;
308 82         211 my @additionalItems_annotations = grep $_->keyword eq 'additionalItems', @annotations;
309 82         250 my @unevaluatedItems_annotations = grep $_->keyword eq 'unevaluatedItems', @annotations;
310              
311             # items, additionalItems or unevaluatedItems already produced a 'true' annotation at this location
312             return 1
313 54 100   54   305 if any { is_type('boolean', $_->annotation) && $_->annotation }
314 82 100       710 @items_annotations, @additionalItems_annotations, @unevaluatedItems_annotations;
315              
316             # otherwise, _eval at every instance item greater than the max of all numeric 'items' annotations
317 77         707 my $last_index = max(-1, grep is_type('integer', $_), map $_->annotation, @items_annotations);
318 77 100       170 return 1 if $last_index == $#{$data};
  77         409  
319              
320 39         109 my $valid = 1;
321 39         85 my @orig_annotations = @{$state->{annotations}};
  39         105  
322 39         97 my @new_annotations;
323 39         93 foreach my $idx ($last_index+1 .. $#{$data}) {
  39         157  
324 43 100       179 if (is_type('boolean', $schema->{unevaluatedItems})) {
325 38 100       711 next if $schema->{unevaluatedItems};
326 33         728 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
327             'additional item not permitted')
328             }
329             else {
330 5         46 my @annotations = @orig_annotations;
331 5 100       90 if ($self->eval($data->[$idx], $schema->{unevaluatedItems},
332             +{ %$state, annotations => \@annotations,
333             data_path => $state->{data_path}.'/'.$idx,
334             schema_path => $state->{schema_path}.'/unevaluatedItems' })) {
335 3         17 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
336 3         25 next;
337             }
338              
339 2         13 $valid = 0;
340             }
341 35 50       299 last if $state->{short_circuit};
342             }
343              
344 39 100       210 return E($state, 'subschema is not valid against all additional items') if not $valid;
345 5         14 push @{$state->{annotations}}, @new_annotations;
  5         16  
346 5         21 return A($state, true);
347             }
348              
349 240     240   864 sub _traverse_keyword_contains { shift->traverse_subschema(@_) }
350              
351             sub _eval_keyword_contains {
352 236     236   639 my ($self, $data, $schema, $state) = @_;
353              
354 236 100       681 return 1 if not is_type('array', $data);
355              
356 161         709 $state->{_num_contains} = 0;
357 161         374 my @orig_annotations = @{$state->{annotations}};
  161         351  
358 161         329 my (@errors, @new_annotations);
359 161         285 foreach my $idx (0 .. $#{$data}) {
  161         544  
360 187         388 my @annotations = @orig_annotations;
361 187 100       2731 if ($self->eval($data->[$idx], $schema->{contains},
362             +{ %$state, errors => \@errors, annotations => \@annotations,
363             data_path => $state->{data_path}.'/'.$idx,
364             schema_path => $state->{schema_path}.'/contains' })) {
365 129         1056 ++$state->{_num_contains};
366 129         347 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
367              
368             last if $state->{short_circuit}
369             and (not exists $schema->{maxContains} or $state->{_num_contains} > $schema->{maxContains})
370 129 100 100     1104 and ($state->{_num_contains} >= ($schema->{minContains}//1));
      100        
      100        
      100        
371             }
372             }
373              
374             # note: no items contained is only valid when minContains is explicitly 0
375 161 100 100     969 if (not $state->{_num_contains} and ($schema->{minContains}//1) > 0) {
      100        
376 65         128 push @{$state->{errors}}, @errors;
  65         161  
377 65         228 return E($state, 'subschema is not valid against any item');
378             }
379              
380 96         209 push @{$state->{annotations}}, @new_annotations;
  96         255  
381 96         390 return 1;
382             }
383              
384 760     760   3162 sub _traverse_keyword_properties { shift->traverse_object_schemas(@_) }
385              
386             sub _eval_keyword_properties {
387 1116     1116   3265 my ($self, $data, $schema, $state) = @_;
388              
389 1116 100       3816 return 1 if not is_type('object', $data);
390              
391 1035         2281 my $valid = 1;
392 1035         1907 my @orig_annotations = @{$state->{annotations}};
  1035         2569  
393 1035         2413 my (@valid_properties, @new_annotations);
394 1035         1859 foreach my $property (sort keys %{$schema->{properties}}) {
  1035         5686  
395 4116 100       8996 next if not exists $data->{$property};
396              
397 706 100       2185 if (is_type('boolean', $schema->{properties}{$property})) {
398 83 100       1604 if ($schema->{properties}{$property}) {
399 41         397 push @valid_properties, $property;
400 41         115 next;
401             }
402              
403 42         667 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
404             _schema_path_suffix => $property }, 'property not permitted');
405             }
406             else {
407 623         4983 my @annotations = @orig_annotations;
408 623 100       4038 if ($self->eval($data->{$property}, $schema->{properties}{$property},
409             +{ %$state, annotations => \@annotations,
410             data_path => jsonp($state->{data_path}, $property),
411             schema_path => jsonp($state->{schema_path}, 'properties', $property) })) {
412 441         1221 push @valid_properties, $property;
413 441         1153 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
414 441         2581 next;
415             }
416              
417 176         948 $valid = 0;
418             }
419 218 100       948 last if $state->{short_circuit};
420             }
421              
422 1029 100       3437 return E($state, 'not all properties are valid') if not $valid;
423 823         1583 push @{$state->{annotations}}, @new_annotations;
  823         1879  
424 823         2665 return A($state, \@valid_properties);
425             }
426              
427             sub _traverse_keyword_patternProperties {
428 221     221   666 my ($self, $schema, $state) = @_;
429              
430 221 50       858 return if not assert_keyword_type($state, $schema, 'object');
431              
432 221         534 foreach my $property (sort keys %{$schema->{patternProperties}}) {
  221         1207  
433 382 100       3374 return if not assert_pattern({ %$state, _schema_path_suffix => $property }, $property);
434 380         2372 $self->traverse_property_schema($schema, $state, $property);
435             }
436             }
437              
438             sub _eval_keyword_patternProperties {
439 215     215   688 my ($self, $data, $schema, $state) = @_;
440              
441 215 100       817 return 1 if not is_type('object', $data);
442              
443 149         359 my $valid = 1;
444 149         299 my @orig_annotations = @{$state->{annotations}};
  149         400  
445 149         342 my (@valid_properties, @new_annotations);
446 149         297 foreach my $property_pattern (sort keys %{$schema->{patternProperties}}) {
  149         763  
447 252         4243 foreach my $property (sort grep m/$property_pattern/, keys %$data) {
448 165 100       759 if (is_type('boolean', $schema->{patternProperties}{$property_pattern})) {
449 78 100       1663 if ($schema->{patternProperties}{$property_pattern}) {
450 38         389 push @valid_properties, $property;
451 38         141 next;
452             }
453              
454 40         599 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
455             _schema_path_suffix => $property_pattern }, 'property not permitted');
456             }
457             else {
458 87         753 my @annotations = @orig_annotations;
459 87 100       563 if ($self->eval($data->{$property}, $schema->{patternProperties}{$property_pattern},
460             +{ %$state, annotations => \@annotations,
461             data_path => jsonp($state->{data_path}, $property),
462             schema_path => jsonp($state->{schema_path}, 'patternProperties', $property_pattern) })) {
463 51         139 push @valid_properties, $property;
464 51         139 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
465 51         336 next;
466             }
467              
468 36         187 $valid = 0;
469             }
470 76 100       477 last if $state->{short_circuit};
471             }
472             }
473              
474 149 100       627 return E($state, 'not all properties are valid') if not $valid;
475 85         172 push @{$state->{annotations}}, @new_annotations;
  85         198  
476 85         497 return A($state, [ uniqstr @valid_properties ]);
477             }
478              
479 324     324   1539 sub _traverse_keyword_additionalProperties { shift->traverse_subschema(@_) }
480              
481             sub _eval_keyword_additionalProperties {
482 392     392   1282 my ($self, $data, $schema, $state) = @_;
483              
484 392 100       1366 return 1 if not is_type('object', $data);
485              
486 270         686 my $valid = 1;
487 270         704 my @orig_annotations = @{$state->{annotations}};
  270         889  
488 270         662 my (@valid_properties, @new_annotations);
489 270         1162 foreach my $property (sort keys %$data) {
490 273 100 100     1181 next if exists $schema->{properties} and exists $schema->{properties}{$property};
491             next if exists $schema->{patternProperties}
492 217 100 100 17   1070 and any { $property =~ /$_/ } keys %{$schema->{patternProperties}};
  17         207  
  17         84  
493              
494 209 100       783 if (is_type('boolean', $schema->{additionalProperties})) {
495 55 100       1296 if ($schema->{additionalProperties}) {
496 8         76 push @valid_properties, $property;
497 8         24 next;
498             }
499              
500 47         724 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
501             'additional property not permitted');
502             }
503             else {
504 154         1419 my @annotations = @orig_annotations;
505 154 100       1185 if ($self->eval($data->{$property}, $schema->{additionalProperties},
506             +{ %$state, annotations => \@annotations,
507             data_path => jsonp($state->{data_path}, $property),
508             schema_path => $state->{schema_path}.'/additionalProperties' })) {
509 79         259 push @valid_properties, $property;
510 79         258 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
511 79         541 next;
512             }
513              
514 75         482 $valid = 0;
515             }
516 122 100       688 last if $state->{short_circuit};
517             }
518              
519 270 100       1042 return E($state, 'not all additional properties are valid') if not $valid;
520 149         265 push @{$state->{annotations}}, @new_annotations;
  149         427  
521 149         652 return A($state, \@valid_properties);
522             }
523              
524             sub _traverse_keyword_unevaluatedProperties {
525 273     273   722 my ($self, $schema, $state) = @_;
526              
527 273         1230 $self->traverse_subschema($schema, $state);
528              
529             # remember that annotations need to be collected in order to evaluate this keyword
530 273         1452 $state->{configs}{collect_annotations} = 1;
531             }
532              
533             sub _eval_keyword_unevaluatedProperties {
534 254     254   813 my ($self, $data, $schema, $state) = @_;
535              
536             abort($state, 'EXCEPTION: "unevaluatedProperties" keyword present, but annotation collection is disabled')
537 254 100       874 if not $state->{collect_annotations};
538              
539             abort($state, 'EXCEPTION: "unevaluatedProperties" keyword present, but short_circuit is enabled: results unreliable')
540 252 100       1030 if $state->{short_circuit};
541              
542 140 100       518 return 1 if not is_type('object', $data);
543              
544             my @evaluated_properties = map {
545 112         452 my $keyword = $_->keyword;
  89         227  
546             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
547 89 50       309 ? @{$_->annotation} : ();
  89         319  
548             } local_annotations($state);
549              
550 112         273 my $valid = 1;
551 112         227 my @orig_annotations = @{$state->{annotations}};
  112         286  
552 112         303 my (@valid_properties, @new_annotations);
553 112         485 foreach my $property (sort keys %$data) {
554 148 100   130   968 next if any { $_ eq $property } @evaluated_properties;
  130         521  
555              
556 69 100       367 if (is_type('boolean', $schema->{unevaluatedProperties})) {
557 64 100       1179 if ($schema->{unevaluatedProperties}) {
558 15         147 push @valid_properties, $property;
559 15         42 next;
560             }
561              
562 49         669 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
563             'additional property not permitted');
564             }
565             else {
566 5         51 my @annotations = @orig_annotations;
567 5 100       38 if ($self->eval($data->{$property}, $schema->{unevaluatedProperties},
568             +{ %$state, annotations => \@annotations,
569             data_path => jsonp($state->{data_path}, $property),
570             schema_path => $state->{schema_path}.'/unevaluatedProperties' })) {
571 3         12 push @valid_properties, $property;
572 3         10 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
573 3         20 next;
574             }
575              
576 2         15 $valid = 0;
577             }
578 51 50       421 last if $state->{short_circuit};
579             }
580              
581 112 100       426 return E($state, 'not all additional properties are valid') if not $valid;
582 68         152 push @{$state->{annotations}}, @new_annotations;
  68         191  
583 68         275 return A($state, \@valid_properties);
584             }
585              
586 147     147   705 sub _traverse_keyword_propertyNames { shift->traverse_subschema(@_) }
587              
588             sub _eval_keyword_propertyNames {
589 139     139   506 my ($self, $data, $schema, $state) = @_;
590              
591 139 100       493 return 1 if not is_type('object', $data);
592              
593 79         224 my $valid = 1;
594 79         157 my @orig_annotations = @{$state->{annotations}};
  79         204  
595 79         176 my @new_annotations;
596 79         329 foreach my $property (sort keys %$data) {
597 54         134 my @annotations = @orig_annotations;
598 54 100       341 if ($self->eval($property, $schema->{propertyNames},
599             +{ %$state, annotations => \@annotations,
600             data_path => jsonp($state->{data_path}, $property),
601             schema_path => $state->{schema_path}.'/propertyNames' })) {
602 17         105 push @new_annotations, @annotations[$#orig_annotations+1 .. $#annotations];
603 17         94 next;
604             }
605              
606 37         178 $valid = 0;
607 37 100       149 last if $state->{short_circuit};
608             }
609              
610 79 100       326 return E($state, 'not all property names are valid') if not $valid;
611 42         85 push @{$state->{annotations}}, @new_annotations;
  42         113  
612 42         167 return 1;
613             }
614              
615             1;
616              
617             __END__
618              
619             =pod
620              
621             =encoding UTF-8
622              
623             =head1 NAME
624              
625             JSON::Schema::Draft201909::Vocabulary::Applicator - Implementation of the JSON Schema Draft 2019-09 Applicator vocabulary
626              
627             =head1 VERSION
628              
629             version 0.028
630              
631             =head1 DESCRIPTION
632              
633             =for Pod::Coverage vocabulary keywords
634              
635             =for stopwords metaschema
636              
637             Implementation of the JSON Schema Draft 2019-09 "Applicator" vocabulary, indicated in metaschemas
638             with the URI C<https://json-schema.org/draft/2019-09/vocab/applicator> and formally specified in
639             L<https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9>.
640              
641             =head1 SUPPORT
642              
643             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Draft201909/issues>.
644              
645             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
646              
647             =head1 AUTHOR
648              
649             Karen Etheridge <ether@cpan.org>
650              
651             =head1 COPYRIGHT AND LICENCE
652              
653             This software is copyright (c) 2020 by Karen Etheridge.
654              
655             This is free software; you can redistribute it and/or modify it under
656             the same terms as the Perl 5 programming language system itself.
657              
658             =cut