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   707 use strict;
  31         80  
  31         976  
2 31     31   175 use warnings;
  31         78  
  31         1657  
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.572';
8              
9 31     31   533 use 5.020;
  31         125  
10 31     31   179 use Moo;
  31         80  
  31         244  
11 31     31   11911 use strictures 2;
  31         251  
  31         1151  
12 31     31   5580 use stable 0.031 'postderef';
  31         536  
  31         190  
13 31     31   4425 use experimental 'signatures';
  31         91  
  31         184  
14 31     31   2486 use if "$]" >= 5.022, experimental => 're_strict';
  31         95  
  31         640  
15 31     31   2895 no if "$]" >= 5.031009, feature => 'indirect';
  31         112  
  31         274  
16 31     31   1559 no if "$]" >= 5.033001, feature => 'multidimensional';
  31         78  
  31         207  
17 31     31   1455 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  31         80  
  31         209  
18 31     31   1429 use List::Util 1.45 qw(any max);
  31         537  
  31         2840  
19 31     31   262 use JSON::Schema::Modern::Utilities qw(is_type jsonp local_annotations E A abort true);
  31         73  
  31         2282  
20 31     31   214 use namespace::clean;
  31         76  
  31         269  
21              
22             with 'JSON::Schema::Modern::Vocabulary';
23              
24             sub vocabulary {
25 160     160 0 618 '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 125 sub keywords ($self, $spec_version) {
  46         113  
  46         102  
  46         112  
32 46 50       314 die 'Unevaluated not implemented in '.$spec_version if $spec_version =~ /^draft[467]$/;
33 46         646 qw(unevaluatedItems unevaluatedProperties);
34             }
35              
36 539     539   985 sub _traverse_keyword_unevaluatedItems ($self, $schema, $state) {
  539         1043  
  539         917  
  539         921  
  539         849  
37 539         2080 $self->traverse_subschema($schema, $state);
38             }
39              
40 542     542   1083 sub _eval_keyword_unevaluatedItems ($self, $data, $schema, $state) {
  542         1007  
  542         963  
  542         981  
  542         913  
  542         832  
41             # these should never happen
42             die '"unevaluatedItems" keyword present, but annotation collection is disabled'
43 542 50       1452 if not $state->{collect_annotations};
44             die '"unevaluatedItems" keyword present, but short_circuit is enabled: results unreliable'
45 542 50       1678 if $state->{short_circuit};
46              
47 542 100       1546 return 1 if not is_type('array', $data);
48              
49 374         1247 my @annotations = local_annotations($state);
50              
51             # a relevant keyword already produced a 'true' annotation at this location
52             my @boolean_annotation_keywords =
53 374 100       1492 $state->{spec_version} eq 'draft2019-09' ? qw(items additionalItems unevaluatedItems)
54             : qw(prefixItems items contains unevaluatedItems);
55 374         687 my %bools; @bools{@boolean_annotation_keywords} = (1)x@boolean_annotation_keywords;
  374         1697  
56             return 1
57 213 100 66 213   1321 if any { $bools{$_->{keyword}} && is_type('boolean', $_->{annotation}) && $_->{annotation} }
58 374 100       2638 @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 290 100       2098 my $max_index_annotation_keyword = $state->{spec_version} eq 'draft2019-09' ? 'items' : 'prefixItems';
63             my $last_index = max(-1, grep is_type('integer', $_),
64 290 100       1118 map +($_->{keyword} eq $max_index_annotation_keyword ? $_->{annotation} : ()), @annotations);
65              
66 290 100       1137 return 1 if $last_index == $data->$#*;
67              
68             my @contains_annotation_indexes = $state->{spec_version} eq 'draft2019-09' ? ()
69 200 100       803 : map +($_->{keyword} eq 'contains' ? $_->{annotation}->@* : ()), @annotations;
    100          
70              
71 200         390 my $valid = 1;
72 200         789 foreach my $idx ($last_index+1 .. $data->$#*) {
73 288 100   154   1496 next if any { $idx == $_ } @contains_annotation_indexes;
  154         306  
74 242 100       986 if (is_type('boolean', $schema->{unevaluatedItems})) {
75 178 100       2360 next if $schema->{unevaluatedItems};
76 158         3492 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
77             'additional item not permitted')
78             }
79             else {
80 64 100       1450 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         308 next;
85             }
86              
87 18         120 $valid = 0;
88             }
89 176 50       1397 last if $state->{short_circuit};
90             }
91              
92 200         857 A($state, true);
93 200 100       804 return E($state, 'subschema is not valid against all additional items') if not $valid;
94 40         235 return 1;
95             }
96              
97 909     909   1796 sub _traverse_keyword_unevaluatedProperties ($self, $schema, $state) {
  909         1826  
  909         1594  
  909         1614  
  909         1558  
98 909         3651 $self->traverse_subschema($schema, $state);
99             }
100              
101 969     969   1974 sub _eval_keyword_unevaluatedProperties ($self, $data, $schema, $state) {
  969         1915  
  969         1870  
  969         1787  
  969         1644  
  969         1624  
102             # these should never happen
103             die '"unevaluatedProperties" keyword present, but annotation collection is disabled'
104 969 50       2809 if not $state->{collect_annotations};
105             die '"unevaluatedProperties" keyword present, but short_circuit is enabled: results unreliable'
106 969 50       2672 if $state->{short_circuit};
107              
108 969 100       2982 return 1 if not is_type('object', $data);
109              
110             my @evaluated_properties = map {
111 826         3021 my $keyword = $_->{keyword};
  661         1507  
112             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
113 661 100       3053 ? $_->{annotation}->@* : ();
114             } local_annotations($state);
115              
116 826         1771 my $valid = 1;
117 826         1512 my @properties;
118 826         3327 foreach my $property (sort keys %$data) {
119 1109 100   904   7337 next if any { $_ eq $property } @evaluated_properties;
  904         3334  
120 558         2471 push @properties, $property;
121              
122 558 100       1808 if (is_type('boolean', $schema->{unevaluatedProperties})) {
123 523 100       7066 next if $schema->{unevaluatedProperties};
124 326         4435 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
125             'additional property not permitted');
126             }
127             else {
128 35 100       508 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         183 next;
133             }
134              
135 13         105 $valid = 0;
136             }
137 339 50       2750 last if $state->{short_circuit};
138             }
139              
140 826         4144 A($state, \@properties);
141 826 100       2806 return E($state, 'not all additional properties are valid') if not $valid;
142 535         2487 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.572
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