File Coverage

blib/lib/JSON/Schema/Modern/Vocabulary/Unevaluated.pm
Criterion Covered Total %
statement 121 122 99.1
branch 49 56 87.5
condition 2 3 66.6
subroutine 23 24 95.8
pod 0 3 0.0
total 195 208 93.7


line stmt bran cond sub pod time code
1 31     31   739 use strict;
  31         86  
  31         1034  
2 31     31   203 use warnings;
  31         84  
  31         1681  
3             package JSON::Schema::Modern::Vocabulary::Unevaluated;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Implementation of the JSON Schema Unevaluated vocabulary
6              
7             our $VERSION = '0.570';
8              
9 31     31   548 use 5.020;
  31         172  
10 31     31   214 use Moo;
  31         102  
  31         254  
11 31     31   12032 use strictures 2;
  31         238  
  31         1305  
12 31     31   5729 use stable 0.031 'postderef';
  31         549  
  31         179  
13 31     31   4536 use experimental 'signatures';
  31         121  
  31         171  
14 31     31   2716 use if "$]" >= 5.022, experimental => 're_strict';
  31         100  
  31         285  
15 31     31   2905 no if "$]" >= 5.031009, feature => 'indirect';
  31         140  
  31         229  
16 31     31   1785 no if "$]" >= 5.033001, feature => 'multidimensional';
  31         173  
  31         227  
17 31     31   1643 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  31         88  
  31         237  
18 31     31   1509 use List::Util 1.45 qw(any max);
  31         565  
  31         2741  
19 31     31   309 use JSON::Schema::Modern::Utilities qw(is_type jsonp local_annotations E A abort true);
  31         98  
  31         2528  
20 31     31   267 use namespace::clean;
  31         104  
  31         288  
21              
22             with 'JSON::Schema::Modern::Vocabulary';
23              
24             sub vocabulary {
25 15     15 0 77 'https://json-schema.org/draft/2020-12/vocab/unevaluated' => 'draft2020-12';
26             }
27              
28 0     0 0 0 sub evaluation_order { 7 }
29              
30             # This vocabulary should be evaluated after the Applicator vocabulary.
31 46     46 0 131 sub keywords ($self, $spec_version) {
  46         116  
  46         100  
  46         102  
32 46 50       307 die 'Unevaluated not implemented in '.$spec_version if $spec_version =~ /^draft[467]$/;
33 46         675 qw(unevaluatedItems unevaluatedProperties);
34             }
35              
36 531     531   946 sub _traverse_keyword_unevaluatedItems ($self, $schema, $state) {
  531         957  
  531         894  
  531         875  
  531         841  
37 531         1861 $self->traverse_subschema($schema, $state);
38             }
39              
40 534     534   967 sub _eval_keyword_unevaluatedItems ($self, $data, $schema, $state) {
  534         979  
  534         938  
  534         860  
  534         831  
  534         855  
41             # these should never happen
42             die '"unevaluatedItems" keyword present, but annotation collection is disabled'
43 534 50       1301 if not $state->{collect_annotations};
44             die '"unevaluatedItems" keyword present, but short_circuit is enabled: results unreliable'
45 534 50       1312 if $state->{short_circuit};
46              
47 534 100       1539 return 1 if not is_type('array', $data);
48              
49 366         1264 my @annotations = local_annotations($state);
50              
51             # a relevant keyword already produced a 'true' annotation at this location
52             my @boolean_annotation_keywords =
53 366 100       1352 $state->{spec_version} eq 'draft2019-09' ? qw(items additionalItems unevaluatedItems)
54             : qw(prefixItems items contains unevaluatedItems);
55 366         647 my %bools; @bools{@boolean_annotation_keywords} = (1)x@boolean_annotation_keywords;
  366         1635  
56             return 1
57 209 100 66 209   1350 if any { $bools{$_->{keyword}} && is_type('boolean', $_->{annotation}) && $_->{annotation} }
58 366 100       2416 @annotations;
59              
60             # otherwise, evaluate at every instance item greater than the max of all 'prefixItems'/numeric
61             # 'items' annotations that isn't in a 'contains' annotation
62 286 100       1927 my $max_index_annotation_keyword = $state->{spec_version} eq 'draft2019-09' ? 'items' : 'prefixItems';
63             my $last_index = max(-1, grep is_type('integer', $_),
64 286 100       1060 map +($_->{keyword} eq $max_index_annotation_keyword ? $_->{annotation} : ()), @annotations);
65              
66 286 100       1117 return 1 if $last_index == $data->$#*;
67              
68             my @contains_annotation_indexes = $state->{spec_version} eq 'draft2019-09' ? ()
69 196 100       686 : map +($_->{keyword} eq 'contains' ? $_->{annotation}->@* : ()), @annotations;
    100          
70              
71 196         366 my $valid = 1;
72 196         738 foreach my $idx ($last_index+1 .. $data->$#*) {
73 284 100   154   1440 next if any { $idx == $_ } @contains_annotation_indexes;
  154         326  
74 238 100       1042 if (is_type('boolean', $schema->{unevaluatedItems})) {
75 174 100       2360 next if $schema->{unevaluatedItems};
76 154         3553 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
77             'additional item not permitted')
78             }
79             else {
80 64 100       1521 if ($self->eval($data->[$idx], $schema->{unevaluatedItems},
81             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
82             schema_path => $state->{schema_path}.'/unevaluatedItems',
83             collect_annotations => $state->{collect_annotations} & ~1 })) {
84 46         337 next;
85             }
86              
87 18         141 $valid = 0;
88             }
89 172 50       1413 last if $state->{short_circuit};
90             }
91              
92 196         944 A($state, true);
93 196 100       813 return E($state, 'subschema is not valid against all additional items') if not $valid;
94 40         222 return 1;
95             }
96              
97 901     901   1712 sub _traverse_keyword_unevaluatedProperties ($self, $schema, $state) {
  901         1759  
  901         1518  
  901         1506  
  901         1440  
98 901         3337 $self->traverse_subschema($schema, $state);
99             }
100              
101 961     961   1994 sub _eval_keyword_unevaluatedProperties ($self, $data, $schema, $state) {
  961         1831  
  961         1678  
  961         1765  
  961         1624  
  961         1622  
102             # these should never happen
103             die '"unevaluatedProperties" keyword present, but annotation collection is disabled'
104 961 50       2624 if not $state->{collect_annotations};
105             die '"unevaluatedProperties" keyword present, but short_circuit is enabled: results unreliable'
106 961 50       2588 if $state->{short_circuit};
107              
108 961 100       2735 return 1 if not is_type('object', $data);
109              
110             my @evaluated_properties = map {
111 818         2908 my $keyword = $_->{keyword};
  653         1489  
112             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
113 653 100       2853 ? $_->{annotation}->@* : ();
114             } local_annotations($state);
115              
116 818         1719 my $valid = 1;
117 818         1410 my @properties;
118 818         3103 foreach my $property (sort keys %$data) {
119 1101 100   900   6916 next if any { $_ eq $property } @evaluated_properties;
  900         3351  
120 554         2322 push @properties, $property;
121              
122 554 100       1766 if (is_type('boolean', $schema->{unevaluatedProperties})) {
123 519 100       6957 next if $schema->{unevaluatedProperties};
124 322         4530 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
125             'additional property not permitted');
126             }
127             else {
128 35 100       451 if ($self->eval($data->{$property}, $schema->{unevaluatedProperties},
129             +{ %$state, data_path => jsonp($state->{data_path}, $property),
130             schema_path => $state->{schema_path}.'/unevaluatedProperties',
131             collect_annotations => $state->{collect_annotations} & ~1 })) {
132 22         168 next;
133             }
134              
135 13         109 $valid = 0;
136             }
137 335 50       2731 last if $state->{short_circuit};
138             }
139              
140 818         4041 A($state, \@properties);
141 818 100       2837 return E($state, 'not all additional properties are valid') if not $valid;
142 531         2339 return 1;
143             }
144              
145             1;
146              
147             __END__
148              
149             =pod
150              
151             =encoding UTF-8
152              
153             =head1 NAME
154              
155             JSON::Schema::Modern::Vocabulary::Unevaluated - Implementation of the JSON Schema Unevaluated vocabulary
156              
157             =head1 VERSION
158              
159             version 0.570
160              
161             =head1 DESCRIPTION
162              
163             =for Pod::Coverage vocabulary evaluation_order keywords
164              
165             =for stopwords metaschema
166              
167             Implementation of the JSON Schema Draft 2020-12 "Unevaluated" vocabulary, indicated in metaschemas
168             with the URI C<https://json-schema.org/draft/2020-12/vocab/unevaluated> and formally specified in
169             L<https://json-schema.org/draft/2020-12/json-schema-core.html#section-11>.
170              
171             Support is also provided for the equivalent Draft 2019-09 keywords in the
172             JSON Schema Draft 2019-09 "Applicator" vocabulary, indicated in metaschemas
173             with the URI C<https://json-schema.org/draft/2019-09/vocab/applicator> and formally specified in
174             L<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-02#section-9>.
175              
176             =for stopwords OpenAPI
177              
178             =head1 SUPPORT
179              
180             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
181              
182             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
183              
184             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
185             server|https://open-api.slack.com>, which are also great resources for finding help.
186              
187             =head1 AUTHOR
188              
189             Karen Etheridge <ether@cpan.org>
190              
191             =head1 COPYRIGHT AND LICENCE
192              
193             This software is copyright (c) 2020 by Karen Etheridge.
194              
195             This is free software; you can redistribute it and/or modify it under
196             the same terms as the Perl 5 programming language system itself.
197              
198             =cut