File Coverage

blib/lib/JSON/Schema/Tiny.pm
Criterion Covered Total %
statement 917 923 99.3
branch 511 564 90.6
condition 341 425 80.2
subroutine 109 110 99.0
pod 1 19 5.2
total 1879 2041 92.0


line stmt bran cond sub pod time code
1             # vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
2 17     17   6884865 use strictures 2;
  17         168  
  17         843  
3             package JSON::Schema::Tiny; # git description: v0.030-3-g571c24a
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Validate data against a schema, minimally
6             # KEYWORDS: JSON Schema data validation structure specification tiny
7              
8             our $VERSION = '0.031';
9              
10 17     17   10281 use 5.020; # for unicode_strings, signatures, postderef features
  17         77  
11 17     17   121 use stable 0.031 'postderef';
  17         300  
  17         123  
12 17     17   4182 use experimental 0.026 qw(signatures args_array_with_signatures);
  17         315  
  17         111  
13 17     17   1770 no autovivification warn => qw(fetch store exists delete);
  17         34  
  17         145  
14 17     17   1533 use if "$]" >= 5.022, experimental => 're_strict';
  17         36  
  17         521  
15 17     17   1503 no if "$]" >= 5.031009, feature => 'indirect';
  17         36  
  17         1427  
16 17     17   172 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         72  
  17         1215  
17 17     17   107 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         27  
  17         1067  
18 17     17   104 use B;
  17         34  
  17         851  
19 17     17   6596 use Ref::Util 0.100 qw(is_plain_arrayref is_plain_hashref is_ref is_plain_arrayref);
  17         32346  
  17         1860  
20 17     17   10486 use Mojo::URL;
  17         2826616  
  17         126  
21 17     17   10538 use Mojo::JSON::Pointer;
  17         17182  
  17         121  
22 17     17   919 use Carp qw(croak carp);
  17         35  
  17         1261  
23 17     17   137 use Storable 'dclone';
  17         84  
  17         1276  
24 17     17   5585 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  17         345294  
  17         3447  
25 17     17   7071 use Feature::Compat::Try;
  17         4584  
  17         155  
26 17     17   1379 use JSON::PP ();
  17         36  
  17         554  
27 17     17   85 use List::Util 1.33 qw(any none);
  17         339  
  17         1511  
28 17     17   105 use Scalar::Util 'looks_like_number';
  17         45  
  17         930  
29 17     17   9063 use builtin::compat qw(blessed created_as_number);
  17         260827  
  17         133  
30 17     17   3630 use if "$]" >= 5.022, POSIX => 'isinf';
  17         34  
  17         10127  
31 17     17   215334 use Math::BigFloat;
  17         2073426  
  17         102  
32 17     17   666027 use namespace::clean;
  17         42  
  17         189  
33 17     17   8032 use Exporter 5.57 'import';
  17         355  
  17         39538  
34              
35             our @EXPORT_OK = qw(evaluate);
36              
37             our $BOOLEAN_RESULT = 0;
38             our $SHORT_CIRCUIT = 0;
39             our $MAX_TRAVERSAL_DEPTH = 50;
40             our $MOJO_BOOLEANS; # deprecated; renamed to $SCALARREF_BOOLEANS
41             our $SCALARREF_BOOLEANS;
42             our $STRINGY_NUMBERS;
43             our $SPECIFICATION_VERSION;
44              
45             my %version_uris = (
46             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12',
47             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09',
48             'http://json-schema.org/draft-07/schema#' => 'draft7',
49             );
50              
51 18     18 0 4940440 sub new ($class, %args) {
  18         48  
  18         55  
  18         37  
52 18         77 bless(\%args, $class);
53             }
54              
55             sub evaluate {
56 12777 50   12777 1 34128086 croak 'evaluate called in void context' if not defined wantarray;
57              
58 12777   66     59642 $SCALARREF_BOOLEANS = $SCALARREF_BOOLEANS // $MOJO_BOOLEANS;
59             local $BOOLEAN_RESULT = $_[0]->{boolean_result} // $BOOLEAN_RESULT,
60             local $SHORT_CIRCUIT = $_[0]->{short_circuit} // $SHORT_CIRCUIT,
61             local $MAX_TRAVERSAL_DEPTH = $_[0]->{max_traversal_depth} // $MAX_TRAVERSAL_DEPTH,
62             local $SCALARREF_BOOLEANS = $_[0]->{scalarref_booleans} // $SCALARREF_BOOLEANS // $_[0]->{mojo_booleans},
63             local $STRINGY_NUMBERS = $_[0]->{stringy_numbers} // $STRINGY_NUMBERS,
64 12777 100 33     313638 local $SPECIFICATION_VERSION = $_[0]->{specification_version} // $SPECIFICATION_VERSION,
      66        
      66        
      33        
      33        
      33        
      66        
      100        
65             shift
66             if blessed($_[0]) and blessed($_[0])->isa(__PACKAGE__);
67              
68 12777 100       35203 if (defined $SPECIFICATION_VERSION) {
69             $SPECIFICATION_VERSION = 'draft'.$SPECIFICATION_VERSION
70 12631 100 100 9   49550 if $SPECIFICATION_VERSION !~ /^draft/ and any { 'draft'.$SPECIFICATION_VERSION eq $_ } values %version_uris;
  9         25  
71              
72 12631 100   25625   102303 croak '$SPECIFICATION_VERSION value is invalid' if none { $SPECIFICATION_VERSION eq $_ } values %version_uris;
  25625         62159  
73             }
74              
75 12776 50       58542 croak 'insufficient arguments' if @_ < 2;
76 12776         32048 my ($data, $schema) = @_;
77              
78 12776   100     61892 my $state = {
79             depth => 0,
80             data_path => '',
81             traversed_schema_path => '', # the accumulated traversal path up to the last $ref traversal
82             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start or the last traversed $ref
83             schema_path => '', # the rest of the path, since the start or the last traversed $ref
84             errors => [],
85             seen => {},
86             short_circuit => $BOOLEAN_RESULT || $SHORT_CIRCUIT,
87             root_schema => $schema, # so we can do $refs within the same document
88             spec_version => $SPECIFICATION_VERSION,
89             };
90              
91 12776         255906 my $valid;
92 12776         25861 try {
93 12776         35599 $valid = _eval_subschema($data, $schema, $state)
94             }
95             catch ($e) {
96 1675 100       5943 if (is_plain_hashref($e)) {
97 1674         6030 push $state->{errors}->@*, $e;
98             }
99             else {
100 1         5 E($state, 'EXCEPTION: '.$e);
101             }
102              
103 1675         5525 $valid = 0;
104             }
105              
106 12776 50 66     60510 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
107              
108             return $BOOLEAN_RESULT ? $valid : +{
109             valid => $valid ? JSON::PP::true : JSON::PP::false,
110 12776 100       108014 $valid ? () : (errors => $state->{errors}),
    100          
    100          
111             };
112             }
113              
114             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
115              
116             # current spec version => { keyword => undef, or arrayref of alternatives }
117             my %removed_keywords = (
118             'draft7' => {
119             id => [ '$id' ],
120             },
121             'draft2019-09' => {
122             id => [ '$id' ],
123             definitions => [ '$defs' ],
124             dependencies => [ qw(dependentSchemas dependentRequired) ],
125             },
126             'draft2020-12' => {
127             id => [ '$id' ],
128             definitions => [ '$defs' ],
129             dependencies => [ qw(dependentSchemas dependentRequired) ],
130             '$recursiveAnchor' => [ '$dynamicAnchor' ],
131             '$recursiveRef' => [ '$dynamicRef' ],
132             additionalItems => [ 'items' ],
133             },
134             );
135              
136 21636     21636   36908 sub _eval_subschema ($data, $schema, $state) {
  21636         36979  
  21636         32285  
  21636         33277  
  21636         30926  
137 21636 50       49546 croak '_eval_subschema called in void context' if not defined wantarray;
138              
139             # do not propagate upwards changes to depth, traversed paths,
140             # but additions to errors are by reference and will be retained
141 21636         132453 $state = { %$state };
142 21636         197758 delete $state->@{'keyword', grep /^_/, keys %$state};
143              
144             abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
145 21636 100       88779 if $state->{depth}++ > $MAX_TRAVERSAL_DEPTH;
146              
147 21633         62723 my $schema_type = get_type($schema);
148 21633 100 66     55289 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
149 20882 100       45198 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
150              
151 20863 100       57624 return 1 if not keys %$schema;
152              
153             # find all schema locations in effect at this data path + canonical_uri combination
154             # if any of them are absolute prefix of this schema location, we are in a loop.
155 20504         49099 my $canonical_uri = canonical_uri($state);
156 20504         63847 my $schema_location = $state->{traversed_schema_path}.$state->{schema_path};
157             {
158 17     17   168 use autovivification qw(fetch store);
  17         40  
  17         204  
  20504         31710  
159             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
160             if grep substr($schema_location, 0, length) eq $_,
161 20504 100       103519 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
162 20502         4248031 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
163             }
164              
165 20502         2950256 my $valid = 1;
166 20502   100     92102 my $spec_version = $state->{spec_version}//'';
167              
168 20502 100 100     825369 foreach my $keyword (
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
    100 100        
169             # CORE KEYWORDS
170             qw($id $schema),
171             !$spec_version || $spec_version ne 'draft7' ? '$anchor' : (),
172             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveAnchor' : (),
173             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicAnchor' : (),
174             '$ref',
175             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveRef' : (),
176             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicRef' : (),
177             !$spec_version || $spec_version ne 'draft7' ? '$vocabulary' : (),
178             '$comment',
179             !$spec_version || $spec_version eq 'draft7' ? 'definitions' : (),
180             !$spec_version || $spec_version ne 'draft7' ? '$defs' : (),
181             # APPLICATOR KEYWORDS
182             qw(allOf anyOf oneOf not if),
183             !$spec_version || $spec_version ne 'draft7' ? 'dependentSchemas' : (),
184             !$spec_version || $spec_version eq 'draft7' ? 'dependencies' : (),
185             !$spec_version || $spec_version !~ qr/^draft(?:7|2019-09)$/ ? 'prefixItems' : (),
186             'items',
187             !$spec_version || $spec_version =~ qr/^draft(?:7|2019-09)$/ ? 'additionalItems' : (),
188             qw(contains properties patternProperties additionalProperties propertyNames),
189             # UNEVALUATED KEYWORDS
190             !$spec_version || $spec_version ne 'draft7' ? qw(unevaluatedItems unevaluatedProperties) : (),
191             # VALIDATOR KEYWORDS
192             qw(type enum const
193             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
194             maxLength minLength pattern
195             maxItems minItems uniqueItems),
196             !$spec_version || $spec_version ne 'draft7' ? qw(maxContains minContains) : (),
197             qw(maxProperties minProperties required),
198             !$spec_version || $spec_version ne 'draft7' ? 'dependentRequired' : (),
199             ) {
200 720298 100       1566314 next if not exists $schema->{$keyword};
201              
202             # keywords adjacent to $ref (except for definitions) are not evaluated before draft2019-09
203             next if $keyword ne '$ref' and $keyword ne 'definitions'
204 34296 100 100     203813 and exists $schema->{'$ref'} and $spec_version eq 'draft7';
      100        
      100        
205              
206 34281         106255 $state->{keyword} = $keyword;
207 34281         79178 my $error_count = $state->{errors}->@*;
208              
209 34281         286318 my $sub = __PACKAGE__->can('_eval_keyword_'.($keyword =~ s/^\$//r));
210 34281 100       112013 if (not $sub->($data, $schema, $state)) {
211             warn 'result is false but there are no errors (keyword: '.$keyword.')'
212 7501 50       28391 if $error_count == $state->{errors}->@*;
213 7501         13117 $valid = 0;
214             }
215              
216 31682 100 100     258522 last if not $valid and $state->{short_circuit};
217             }
218              
219             # check for previously-supported but now removed keywords
220 17903   100     155814 foreach my $keyword (sort keys(($removed_keywords{$spec_version}//{})->%*)) {
221 62636 100       147854 next if not exists $schema->{$keyword};
222 214         653 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
223             .canonical_uri($state).'")';
224 214 50       29468 if (my $alternates = $removed_keywords{$spec_version}->{$keyword}) {
225 214         1257 my @list = map '"'.$_.'"', @$alternates;
226 214 50       710 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
227 214 100       786 splice(@list, -1, 0, 'or') if @list > 1;
228 214         771 $message .= ': this should be rewritten as '.join(' ', @list);
229             }
230 214         38313 carp $message;
231             }
232              
233 17903         163179 return $valid;
234             }
235              
236             # KEYWORD IMPLEMENTATIONS
237              
238 5644     5644   9597 sub _eval_keyword_schema ($data, $schema, $state) {
  5644         9849  
  5644         8418  
  5644         7967  
  5644         8062  
239 5644         17854 assert_keyword_type($state, $schema, 'string');
240 5644         19129 assert_uri($state, $schema);
241              
242             return abort($state, '$schema can only appear at the schema resource root')
243 5644 100       24401 if length($state->{schema_path});
244              
245 5643         20702 my $spec_version = $version_uris{$schema->{'$schema'}};
246 5643 100       15002 abort($state, 'custom $schema URIs are not supported (must be one of: %s',
247             join(', ', map '"'.$_.'"', keys %version_uris))
248             if not $spec_version;
249              
250 5614 100 100     22601 abort($state, '"$schema" indicates a different version than that requested by $JSON::Schema::Tiny::SPECIFICATION_VERSION')
251             if defined $SPECIFICATION_VERSION and $SPECIFICATION_VERSION ne $spec_version;
252              
253             # we special-case this because the check in _eval for older drafts + $ref has already happened
254             abort($state, '$schema and $ref cannot be used together in older drafts')
255 5613 100 100     22340 if exists $schema->{'$ref'} and $spec_version eq 'draft7';
256              
257 5612         21651 $state->{spec_version} = $spec_version;
258             }
259              
260 1699     1699   3232 sub _eval_keyword_ref ($data, $schema, $state) {
  1699         3445  
  1699         2822  
  1699         2621  
  1699         2637  
261 1699         5490 assert_keyword_type($state, $schema, 'string');
262 1699         5808 assert_uri_reference($state, $schema);
263              
264 1699         11825 my $uri = Mojo::URL->new($schema->{$state->{keyword}})->to_abs($state->{initial_schema_uri});
265             abort($state, '%ss to anchors are not supported', $state->{keyword})
266 1699 100 100     824495 if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
267              
268             # the base of the $ref uri must be the same as the base of the root schema
269             # unfortunately this means that many uses of $ref won't work, because we don't
270             # track the locations of $ids in this or other documents.
271             abort($state, 'only same-document, same-base JSON pointers are supported in %s', $state->{keyword})
272 1601 100 100     26539 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
273              
274 1132   100     721641 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment//'');
275 1132 100       62374 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
276              
277             return _eval_subschema($data, $subschema,
278             +{ %$state,
279             traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/'.$state->{keyword},
280 1127         18392 initial_schema_uri => $uri,
281             schema_path => '',
282             });
283             }
284              
285 52     52   97 sub _eval_keyword_recursiveRef ($data, $schema, $state) {
  52         90  
  52         78  
  52         70  
  52         89  
286 52         161 assert_keyword_type($state, $schema, 'string');
287 52         183 assert_uri_reference($state, $schema);
288              
289 52         356 my $uri = Mojo::URL->new($schema->{'$recursiveRef'})->to_abs($state->{initial_schema_uri});
290 52 50 50     23396 abort($state, '$recursiveRefs to anchors are not supported')
291             if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
292              
293             # the base of the $recursiveRef uri must be the same as the base of the root schema.
294             # unfortunately this means that nearly all usecases of $recursiveRef won't work, because we don't
295             # track the locations of $ids in this or other documents.
296             abort($state, 'only same-document, same-base JSON pointers are supported in $recursiveRef')
297 52 100 100     659 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
298              
299 8         3868 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
300 8 50       193 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
301              
302 8 50 33     38 if (is_type('boolean', $subschema->{'$recursiveAnchor'}) and $subschema->{'$recursiveAnchor'}) {
303             $uri = Mojo::URL->new($schema->{'$recursiveRef'})
304 0   0     0 ->to_abs($state->{recursive_anchor_uri} // $state->{initial_schema_uri});
305 0         0 $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
306 0 0       0 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
307             }
308              
309             return _eval_subschema($data, $subschema,
310             +{ %$state,
311 8         126 traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/$recursiveRef',
312             initial_schema_uri => $uri,
313             schema_path => '',
314             });
315             }
316              
317 12     12   45 sub _eval_keyword_dynamicRef { goto \&_eval_keyword_ref }
318              
319 670     670   1532 sub _eval_keyword_id ($data, $schema, $state) {
  670         1345  
  670         1134  
  670         1083  
  670         1156  
320 670         2320 assert_keyword_type($state, $schema, 'string');
321 670         2289 assert_uri_reference($state, $schema);
322              
323 670         3702 my $uri = Mojo::URL->new($schema->{'$id'});
324              
325 670 100 100     82945 if (($state->{spec_version}//'') eq 'draft7') {
326 133 100       419 if (length($uri->fragment)) {
327 3 50       27 abort($state, '$id cannot change the base uri at the same time as declaring an anchor')
328             if length($uri->clone->fragment(undef));
329              
330 3 100       680 abort($state, '$id value does not match required syntax')
331             if $uri->fragment !~ m/^[A-Za-z][A-Za-z0-9_:.-]*$/;
332              
333 2         36 return 1;
334             }
335             }
336             else {
337 537 100       1697 abort($state, '$id value "%s" cannot have a non-empty fragment', $uri) if length $uri->fragment;
338             }
339              
340 665         4648 $uri->fragment(undef);
341 665 100       4867 return E($state, '$id cannot be empty') if not length $uri;
342              
343 641 100       130958 $state->{initial_schema_uri} = $uri->is_abs ? $uri : $uri->to_abs($state->{initial_schema_uri});
344 641         63062 $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path};
345 641         1643 $state->{schema_path} = '';
346              
347 641         2489 return 1;
348             }
349              
350 12     12   21 sub _eval_keyword_anchor ($data, $schema, $state) {
  12         23  
  12         17  
  12         17  
  12         14  
351 12         38 assert_keyword_type($state, $schema, 'string');
352              
353             return 1 if
354             (!$state->{spec_version} or $state->{spec_version} eq 'draft2019-09')
355             and ($schema->{'$anchor'}//'') =~ /^[A-Za-z][A-Za-z0-9_:.-]*$/
356             or
357             (!$state->{spec_version} or $state->{spec_version} eq 'draft2020-12')
358 12 50 66     260 and ($schema->{'$anchor'}//'') =~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
      50        
      66        
      33        
      50        
      33        
      66        
359              
360 0         0 abort($state, '$anchor value does not match required syntax');
361             }
362              
363 92     92   180 sub _eval_keyword_recursiveAnchor ($data, $schema, $state) {
  92         186  
  92         145  
  92         159  
  92         132  
364 92         276 assert_keyword_type($state, $schema, 'boolean');
365 92 100 100     558 return 1 if not $schema->{'$recursiveAnchor'} or exists $state->{recursive_anchor_uri};
366              
367             # this is required because the location is used as the base URI for future resolution
368             # of $recursiveRef, and the fragment would be disregarded in the base
369             abort($state, '"$recursiveAnchor" keyword used without "$id"')
370 51 50       629 if not exists $schema->{'$id'};
371              
372             # record the canonical location of the current position, to be used against future resolution
373             # of a $recursiveRef uri -- as if it was the current location when we encounter a $ref.
374 51         139 $state->{recursive_anchor_uri} = canonical_uri($state);
375              
376 51         145 return 1;
377             }
378              
379 10     10   17 sub _eval_keyword_dynamicAnchor ($data, $schema, $state) {
  10         18  
  10         17  
  10         14  
  10         15  
380 10 50       24 return if not assert_keyword_type($state, $schema, 'string');
381              
382             abort($state, '$dynamicAnchor value does not match required syntax')
383 10 50       70 if $schema->{'$dynamicAnchor'} !~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
384 10         28 return 1;
385             }
386              
387 4     4   8 sub _eval_keyword_vocabulary ($data, $schema, $state) {
  4         9  
  4         7  
  4         7  
  4         9  
388 4         17 assert_keyword_type($state, $schema, 'object');
389              
390 4         25 foreach my $uri (sort keys $schema->{'$vocabulary'}->%*) {
391             abort({ %$state, _schema_path_suffix => $uri }, '$vocabulary value at "%s" is not a boolean', $uri)
392 4 50       21 if not is_type('boolean', $schema->{'$vocabulary'}{$uri});
393              
394 4         17 assert_uri($state, undef, $uri);
395             }
396              
397             abort($state, '$vocabulary can only appear at the schema resource root')
398 4 50       22 if length($state->{schema_path});
399              
400             abort($state, '$vocabulary can only appear at the document root')
401 4 50       24 if length($state->{traversed_schema_path}.$state->{schema_path});
402              
403 4         13 return 1;
404             }
405              
406 288     288   584 sub _eval_keyword_comment ($data, $schema, $state) {
  288         609  
  288         554  
  288         481  
  288         480  
407 288         906 assert_keyword_type($state, $schema, 'string');
408 288         859 return 1;
409             }
410              
411 150     150   870 sub _eval_keyword_definitions { goto \&_eval_keyword_defs }
412              
413 677     677   1349 sub _eval_keyword_defs ($data, $schema, $state) {
  677         1441  
  677         1281  
  677         1437  
  677         1163  
414 677         2314 assert_keyword_type($state, $schema, 'object');
415 675         1956 return 1;
416             }
417              
418 3706     3706   6476 sub _eval_keyword_type ($data, $schema, $state) {
  3706         6732  
  3706         5615  
  3706         5412  
  3706         6184  
419 3706 100       11963 if (is_plain_arrayref($schema->{type})) {
420 163 50       737 abort($state, 'type array is empty') if not $schema->{type}->@*;
421 163         543 foreach my $type ($schema->{type}->@*) {
422             abort($state, 'unrecognized type "%s"', $type//'')
423 344 100 50 1397   1735 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  1397   100     3532  
424             }
425 157 50       616 abort($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
426              
427 157         337 my $type = get_type($data);
428             return 1 if any {
429 292 50 100 292   2704 $type eq $_ or ($_ eq 'number' and $type eq 'integer')
      100        
      66        
      66        
      100        
      66        
      66        
      66        
      33        
430             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
431             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
432             or ($_ eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR')
433 157 100       1311 } $schema->{type}->@*;
434 92         946 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
435             }
436             else {
437 3543         12059 assert_keyword_type($state, $schema, 'string');
438             abort($state, 'unrecognized type "%s"', $schema->{type}//'')
439 3537 100 50 16514   26930 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  16514   50     40311  
440              
441 3535         17128 my $type = get_type($data);
442             return 1 if $type eq $schema->{type} or ($schema->{type} eq 'number' and $type eq 'integer')
443             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
444             and ($schema->{type} eq 'number' or ($schema->{type} eq 'integer' and $data == int($data))))
445 3535 100 100     28334 or ($schema->{type} eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR');
      100        
      100        
      66        
      66        
      66        
      66        
      100        
      100        
      66        
446 985         4608 return E($state, 'got %s, not %s', $type, $schema->{type});
447             }
448             }
449              
450 483     483   944 sub _eval_keyword_enum ($data, $schema, $state) {
  483         998  
  483         858  
  483         777  
  483         810  
451 483         1575 assert_keyword_type($state, $schema, 'array');
452              
453 483         997 my @s; my $idx = 0;
  483         853  
454 483 100   933   4652 return 1 if any { is_equal($data, $_, $s[$idx++] = {}) } $schema->{enum}->@*;
  933         18787  
455              
456             return E($state, 'value does not match'
457 219 100       2657 .(!(grep $_->{path}, @s) ? ''
458             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
459             }
460              
461 1055     1055   1953 sub _eval_keyword_const ($data, $schema, $state) {
  1055         2240  
  1055         1780  
  1055         1604  
  1055         1581  
462 1055 100       4369 return 1 if is_equal($data, $schema->{const}, my $s = {});
463 479 100       9627 return E($state, 'value does not match'.($s->{path} ? " (at '$s->{path}': $s->{error})" : ''));
464             }
465              
466 832     832   1585 sub _eval_keyword_multipleOf ($data, $schema, $state) {
  832         1775  
  832         1441  
  832         1366  
  832         1326  
467 832         2823 assert_keyword_type($state, $schema, 'number');
468 830 50       3405 abort($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
469              
470             return 1 if not is_type('number', $data)
471             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data)
472 830 50 66     26453 and do { $data = 0+$data; 1 });
  2   66     13  
  2   33     11  
      100        
473              
474             # if either value is a float, use the bignum library for the calculation
475 640 100 100     1512 if (is_bignum($data) or is_bignum($schema->{multipleOf})
      66        
      100        
476             or get_type($data) eq 'number' or get_type($schema->{multipleOf}) eq 'number') {
477 52 100       126 $data = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
478 52 100       5040 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
479 52         2216 my ($quotient, $remainder) = $data->bdiv($divisor);
480 52 50       89836 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
481 52 100       723 return 1 if $remainder == 0;
482             }
483             else {
484 588         2142 my $quotient = $data / $schema->{multipleOf};
485 588 50       5801 return E($state, 'overflow while calculating quotient')
    50          
486             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
487 588 100       2868 return 1 if int($quotient) == $quotient;
488             }
489              
490 297         12239 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
491             }
492              
493 585     585   1111 sub _eval_keyword_maximum ($data, $schema, $state) {
  585         1127  
  585         1056  
  585         939  
  585         867  
494 585         1888 assert_keyword_type($state, $schema, 'number');
495 583 50 66     1422 return 1 if not is_type('number', $data)
      66        
      100        
496             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
497 388 100       1742 return 1 if 0+$data <= $schema->{maximum};
498 172         14339 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
499             }
500              
501 483     483   819 sub _eval_keyword_exclusiveMaximum ($data, $schema, $state) {
  483         813  
  483         717  
  483         674  
  483         617  
502 483         1347 assert_keyword_type($state, $schema, 'number');
503 481 50 66     991 return 1 if not is_type('number', $data)
      66        
      100        
504             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
505 292 100       1311 return 1 if 0+$data < $schema->{exclusiveMaximum};
506 154         16873 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
507             }
508              
509 713     713   1262 sub _eval_keyword_minimum ($data, $schema, $state) {
  713         1263  
  713         1060  
  713         996  
  713         1025  
510 713         2103 assert_keyword_type($state, $schema, 'number');
511 711 50 66     1637 return 1 if not is_type('number', $data)
      66        
      100        
512             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
513 507 100       2288 return 1 if 0+$data >= $schema->{minimum};
514 242         30314 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
515             }
516              
517 423     423   748 sub _eval_keyword_exclusiveMinimum ($data, $schema, $state) {
  423         850  
  423         702  
  423         680  
  423         653  
518 423         1443 assert_keyword_type($state, $schema, 'number');
519 421 50 66     1042 return 1 if not is_type('number', $data)
      66        
      100        
520             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
521 232 100       1146 return 1 if 0+$data > $schema->{exclusiveMinimum};
522 124         15677 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
523             }
524              
525 561     561   1142 sub _eval_keyword_maxLength ($data, $schema, $state) {
  561         1099  
  561         966  
  561         845  
  561         830  
526 561         1907 assert_non_negative_integer($schema, $state);
527              
528 561 100       1262 return 1 if not is_type('string', $data);
529 352 100       1733 return 1 if length($data) <= $schema->{maxLength};
530 162         3223 return E($state, 'length is greater than %d', $schema->{maxLength});
531             }
532              
533 512     512   909 sub _eval_keyword_minLength ($data, $schema, $state) {
  512         977  
  512         985  
  512         795  
  512         749  
534 512         1770 assert_non_negative_integer($schema, $state);
535              
536 512 100       1154 return 1 if not is_type('string', $data);
537 302 100       1359 return 1 if length($data) >= $schema->{minLength};
538 142         3308 return E($state, 'length is less than %d', $schema->{minLength});
539             }
540              
541 899     899   1527 sub _eval_keyword_pattern ($data, $schema, $state) {
  899         1834  
  899         1552  
  899         1495  
  899         1438  
542 899         2653 assert_keyword_type($state, $schema, 'string');
543 899         3297 assert_pattern($state, $schema->{pattern});
544              
545 898 100       1964 return 1 if not is_type('string', $data);
546 671 100       7659 return 1 if $data =~ m/(?:$schema->{pattern})/;
547 313         1058 return E($state, 'pattern does not match');
548             }
549              
550 425     425   803 sub _eval_keyword_maxItems ($data, $schema, $state) {
  425         756  
  425         717  
  425         632  
  425         639  
551 425         1413 assert_non_negative_integer($schema, $state);
552              
553 425 100       974 return 1 if not is_type('array', $data);
554 256 100       1059 return 1 if @$data <= $schema->{maxItems};
555 122 100       2683 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
556             }
557              
558 424     424   715 sub _eval_keyword_minItems ($data, $schema, $state) {
  424         707  
  424         689  
  424         636  
  424         586  
559 424         1421 assert_non_negative_integer($schema, $state);
560              
561 424 100       914 return 1 if not is_type('array', $data);
562 257 100       1070 return 1 if @$data >= $schema->{minItems};
563 124 100       2843 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
564             }
565              
566 775     775   1408 sub _eval_keyword_uniqueItems ($data, $schema, $state) {
  775         1381  
  775         1324  
  775         1170  
  775         2227  
567 775         2397 assert_keyword_type($state, $schema, 'boolean');
568 775 100       1910 return 1 if not is_type('array', $data);
569 614 100       3444 return 1 if not $schema->{uniqueItems};
570 449 100       4950 return 1 if is_elements_unique($data, my $equal_indices = []);
571 207         760 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
572             }
573              
574 84     84   168 sub _eval_keyword_maxContains ($data, $schema, $state) {
  84         148  
  84         143  
  84         149  
  84         135  
575 84         364 assert_non_negative_integer($schema, $state);
576 84 100       349 return 1 if not exists $state->{_num_contains};
577 76 50       273 return 1 if not is_type('array', $data);
578              
579             return E($state, 'contains too many matching items')
580 76 100       406 if $state->{_num_contains} > $schema->{maxContains};
581              
582 44         1760 return 1;
583             }
584              
585 102     102   193 sub _eval_keyword_minContains ($data, $schema, $state) {
  102         226  
  102         169  
  102         212  
  102         159  
586 102         391 assert_non_negative_integer($schema, $state);
587 102 100       506 return 1 if not exists $state->{_num_contains};
588 94 50       296 return 1 if not is_type('array', $data);
589              
590             return E($state, 'contains too few matching items')
591 94 100       460 if $state->{_num_contains} < $schema->{minContains};
592              
593 60         2253 return 1;
594             }
595              
596 340     340   603 sub _eval_keyword_maxProperties ($data, $schema, $state) {
  340         688  
  340         492  
  340         548  
  340         473  
597 340         1136 assert_non_negative_integer($schema, $state);
598              
599 340 100       705 return 1 if not is_type('object', $data);
600 202 100       930 return 1 if keys %$data <= $schema->{maxProperties};
601             return E($state, 'more than %d propert%s', $schema->{maxProperties},
602 98 100       3030 $schema->{maxProperties} > 1 ? 'ies' : 'y');
603             }
604              
605 340     340   617 sub _eval_keyword_minProperties ($data, $schema, $state) {
  340         603  
  340         565  
  340         580  
  340         504  
606 340         1263 assert_non_negative_integer($schema, $state);
607              
608 340 100       694 return 1 if not is_type('object', $data);
609 202 100       1003 return 1 if keys %$data >= $schema->{minProperties};
610             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
611 98 100       2671 $schema->{minProperties} > 1 ? 'ies' : 'y');
612             }
613              
614 1414     1414   2635 sub _eval_keyword_required ($data, $schema, $state) {
  1414         2520  
  1414         2213  
  1414         2198  
  1414         2111  
615 1414         4075 assert_keyword_type($state, $schema, 'array');
616             abort($state, '"required" element is not a string')
617 1414 50   1600   11195 if any { !is_type('string', $_) } $schema->{required}->@*;
  1600         3465  
618 1414 50       8825 abort($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
619              
620 1414 100       3346 return 1 if not is_type('object', $data);
621              
622 1264         7800 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
623 1264 100       4504 return 1 if not @missing;
624 566 100       3376 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
625             }
626              
627 271     271   494 sub _eval_keyword_dependentRequired ($data, $schema, $state) {
  271         539  
  271         445  
  271         389  
  271         387  
628 271         759 assert_keyword_type($state, $schema, 'object');
629              
630 271         1349 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
631             E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
632 287 50       927 if not is_type('array', $schema->{dependentRequired}{$property});
633              
634 287         1266 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
635             abort({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
636 301 100       1070 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
637             }
638              
639             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
640 286 50       1115 if not is_elements_unique($schema->{dependentRequired}{$property});
641             }
642              
643 270 100       646 return 1 if not is_type('object', $data);
644              
645 173         372 my $valid = 1;
646 173         730 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
647 189 100       694 next if not exists $data->{$property};
648              
649 153 100       1121 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
650 79 100       1141 $valid = E({ %$state, _schema_path_suffix => $property },
651             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
652             }
653             }
654              
655 173 100       609 return 1 if $valid;
656 79         220 return E($state, 'not all dependencies are satisfied');
657             }
658              
659 575     575   1176 sub _eval_keyword_allOf ($data, $schema, $state) {
  575         1203  
  575         1106  
  575         925  
  575         923  
660 575         2042 assert_array_schemas($schema, $state);
661              
662 575         1053 my @invalid;
663 575         2891 foreach my $idx (0..$schema->{allOf}->$#*) {
664             next if _eval_subschema($data, $schema->{allOf}[$idx],
665 843 100       12085 +{ %$state, schema_path => $state->{schema_path}.'/allOf/'.$idx });
666              
667 209         1305 push @invalid, $idx;
668 209 100       964 last if $state->{short_circuit};
669             }
670              
671 404 100       2373 return 1 if @invalid == 0;
672              
673 169         467 my $pl = @invalid > 1;
674 169 100       1107 return E($state, 'subschema%s %s %s not valid', $pl?'s':'', join(', ', @invalid), $pl?'are':'is');
    100          
675             }
676              
677 433     433   824 sub _eval_keyword_anyOf ($data, $schema, $state) {
  433         829  
  433         878  
  433         792  
  433         639  
678 433         1478 assert_array_schemas($schema, $state);
679              
680 433         849 my $valid = 0;
681 433         750 my @errors;
682 433         2586 foreach my $idx (0..$schema->{anyOf}->$#*) {
683             next if not _eval_subschema($data, $schema->{anyOf}[$idx],
684 760 100       10819 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/anyOf/'.$idx });
685 239         1655 ++$valid;
686 239 100       917 last if $state->{short_circuit};
687             }
688              
689 294 100       1218 return 1 if $valid;
690 92         371 push $state->{errors}->@*, @errors;
691 92         272 return E($state, 'no subschemas are valid');
692             }
693              
694 509     509   1178 sub _eval_keyword_oneOf ($data, $schema, $state) {
  509         1016  
  509         905  
  509         833  
  509         830  
695 509         2083 assert_array_schemas($schema, $state);
696              
697 509         1122 my (@valid, @errors);
698 509         2381 foreach my $idx (0..$schema->{oneOf}->$#*) {
699             next if not _eval_subschema($data, $schema->{oneOf}[$idx],
700 1061 100       15605 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/oneOf/'.$idx });
701 377         2564 push @valid, $idx;
702 377 100 100     1905 last if @valid > 1 and $state->{short_circuit};
703             }
704              
705 358 100       1753 return 1 if @valid == 1;
706              
707 201 100       616 if (not @valid) {
708 123         538 push $state->{errors}->@*, @errors;
709 123         413 return E($state, 'no subschemas are valid');
710             }
711             else {
712 78         6784 return E($state, 'multiple subschemas are valid: '.join(', ', @valid));
713             }
714             }
715              
716 293     293   546 sub _eval_keyword_not ($data, $schema, $state) {
  293         598  
  293         464  
  293         462  
  293         425  
717 293 100 66     1060 return !$schema->{not} || E($state, 'subschema is true') if is_type('boolean', $schema->{not});
718              
719             return 1 if not _eval_subschema($data, $schema->{not},
720 181 100       2547 +{ %$state, schema_path => $state->{schema_path}.'/not', short_circuit => 1, errors => [] });
721              
722 135         802 return E($state, 'subschema is valid');
723             }
724              
725 326     326   715 sub _eval_keyword_if ($data, $schema, $state) {
  326         667  
  326         614  
  326         520  
  326         536  
726 326 100 100     1637 return 1 if not exists $schema->{then} and not exists $schema->{else};
727             my $keyword = _eval_subschema($data, $schema->{if},
728 282 100       3738 +{ %$state, schema_path => $state->{schema_path}.'/if', short_circuit => 1, errors => [] })
729             ? 'then' : 'else';
730              
731 282 100       2317 return 1 if not exists $schema->{$keyword};
732              
733             return $schema->{$keyword} || E({ %$state, keyword => $keyword }, 'subschema is false')
734 224 100 66     834 if is_type('boolean', $schema->{$keyword});
735              
736             return 1 if _eval_subschema($data, $schema->{$keyword},
737 192 100       2341 +{ %$state, schema_path => $state->{schema_path}.'/'.$keyword });
738 62         714 return E({ %$state, keyword => $keyword }, 'subschema is not valid');
739             }
740              
741 337     337   655 sub _eval_keyword_dependentSchemas ($data, $schema, $state) {
  337         714  
  337         662  
  337         616  
  337         524  
742 337         1075 assert_keyword_type($state, $schema, 'object');
743              
744 337 100       750 return 1 if not is_type('object', $data);
745              
746 213         407 my $valid = 1;
747 213         2300 foreach my $property (sort keys $schema->{dependentSchemas}->%*) {
748             next if not exists $data->{$property}
749             or _eval_subschema($data, $schema->{dependentSchemas}{$property},
750 263 100 100     1828 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependentSchemas', $property) });
751              
752 97         450 $valid = 0;
753 97 100       379 last if $state->{short_circuit};
754             }
755              
756 213 100       865 return E($state, 'not all dependencies are satisfied') if not $valid;
757 116         383 return 1;
758             }
759              
760 186     186   319 sub _eval_keyword_dependencies ($data, $schema, $state) {
  186         341  
  186         295  
  186         264  
  186         268  
761 186         574 assert_keyword_type($state, $schema, 'object');
762              
763 186 100       376 return 1 if not is_type('object', $data);
764              
765 119         243 my $valid = 1;
766 119         546 foreach my $property (sort keys $schema->{dependencies}->%*) {
767 166 100       515 if (is_type('array', $schema->{dependencies}{$property})) {
768             # as in dependentRequired
769              
770 52         171 foreach my $index (0..$schema->{dependencies}{$property}->$#*) {
771             $valid = E({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
772 62 50       163 if not is_type('string', $schema->{dependencies}{$property}[$index]);
773             }
774              
775             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
776 52 50       152 if not is_elements_unique($schema->{dependencies}{$property});
777              
778 52 100       146 next if not exists $data->{$property};
779              
780 24 100       144 if (my @missing = grep !exists($data->{$_}), $schema->{dependencies}{$property}->@*) {
781 14 100       158 $valid = E({ %$state, _schema_path_suffix => $property },
782             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
783             }
784             }
785             else {
786             # as in dependentSchemas
787             next if not exists $data->{$property}
788             or _eval_subschema($data, $schema->{dependencies}{$property},
789 114 100 100     733 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependencies', $property) });
790              
791 47         258 $valid = 0;
792 47 100       198 last if $state->{short_circuit};
793             }
794             }
795              
796 119 100       463 return 1 if $valid;
797 59         139 return E($state, 'not all dependencies are satisfied');
798             }
799              
800 411     411   808 sub _eval_keyword_prefixItems ($data, $schema, $state) {
  411         890  
  411         773  
  411         855  
  411         678  
801 411 50       1607 return if not assert_array_schemas($schema, $state);
802 411         2030 goto \&_eval_keyword__items_array_schemas;
803             }
804              
805 1304     1304   2413 sub _eval_keyword_items ($data, $schema, $state) {
  1304         2383  
  1304         2190  
  1304         1863  
  1304         1999  
806 1304 100       4618 if (is_plain_arrayref($schema->{items})) {
807             abort($state, 'array form of "items" not supported in %s', $state->{spec_version})
808 700 100 100     2583 if ($state->{spec_version}//'') eq 'draft2020-12';
809              
810 699         2963 goto \&_eval_keyword__items_array_schemas;
811             }
812              
813 604   100     3377 $state->{_last_items_index} //= -1;
814 604         2850 goto \&_eval_keyword__items_schema;
815             }
816              
817 219     219   415 sub _eval_keyword_additionalItems ($data, $schema, $state) {
  219         392  
  219         356  
  219         366  
  219         303  
818 219 100       774 return 1 if not exists $state->{_last_items_index};
819 191         926 goto \&_eval_keyword__items_schema;
820             }
821              
822             # prefixItems (draft 2020-12), array-based items (all drafts)
823 1110     1110   1975 sub _eval_keyword__items_array_schemas ($data, $schema, $state) {
  1110         2005  
  1110         1744  
  1110         1671  
  1110         1478  
824 1110 50       3967 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
825 1110 100       2655 return 1 if not is_type('array', $data);
826              
827 897         1763 my $valid = 1;
828              
829 897         3405 foreach my $idx (0..$data->$#*) {
830 1581 100       7299 last if $idx > $schema->{$state->{keyword}}->$#*;
831 1294         3888 $state->{_last_items_index} = $idx;
832              
833 1294 100       4389 if (is_type('boolean', $schema->{$state->{keyword}}[$idx])) {
834 286 100       1584 next if $schema->{$state->{keyword}}[$idx];
835 108         2223 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx,
836             _schema_path_suffix => $idx }, 'item not permitted');
837             }
838             else {
839             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}}[$idx],
840             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
841 1008 100       15751 schema_path => $state->{schema_path}.'/'.$state->{keyword}.'/'.$idx });
842             }
843              
844 175         872 $valid = 0;
845             last if $state->{short_circuit} and not exists $schema->{
846             $state->{keyword} eq 'prefixItems' ? 'items'
847 175 50 100     1353 : $state->{keyword} eq 'items' ? 'additionalItems' : die
    100          
    100          
848             };
849             }
850              
851 897 100       2649 return E($state, 'not all items are valid') if not $valid;
852 725         2216 return 1;
853             }
854              
855             # schema-based items (all drafts), and additionalItems (drafts 4,6,7,2019-09)
856 795     795   1377 sub _eval_keyword__items_schema ($data, $schema, $state) {
  795         1393  
  795         1350  
  795         1172  
  795         1346  
857 795 100       2194 return 1 if not is_type('array', $data);
858 691 100       3142 return 1 if $state->{_last_items_index} == $data->$#*;
859              
860 447         945 my $valid = 1;
861 447         2001 foreach my $idx ($state->{_last_items_index}+1 .. $data->$#*) {
862 676 100 100     2724 if (is_type('boolean', $schema->{$state->{keyword}})
863             and ($state->{keyword} eq 'additionalItems')) {
864 32 100       196 next if $schema->{$state->{keyword}};
865             $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
866             '%sitem not permitted',
867 26 50 33     694 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '');
868             }
869             else {
870             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}},
871             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
872 644 100       10448 schema_path => $state->{schema_path}.'/'.$state->{keyword} });
873 219         1162 $valid = 0;
874             }
875              
876 245 100       1251 last if $state->{short_circuit};
877             }
878              
879 382         1441 $state->{_last_items_index} = $data->$#*;
880              
881             return E($state, 'subschema is not valid against all %sitems',
882 382 100 100     2276 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '')
    100          
883             if not $valid;
884 179         518 return 1;
885             }
886              
887 717     717   1265 sub _eval_keyword_contains ($data, $schema, $state) {
  717         1301  
  717         1161  
  717         1178  
  717         1250  
888 717 100       2004 return 1 if not is_type('array', $data);
889              
890 504         2068 $state->{_num_contains} = 0;
891 504         898 my @errors;
892 504         1862 foreach my $idx (0..$data->$#*) {
893 622 100       11560 if (_eval_subschema($data->[$idx], $schema->{contains},
894             +{ %$state, errors => \@errors,
895             data_path => $state->{data_path}.'/'.$idx,
896             schema_path => $state->{schema_path}.'/contains' })) {
897 390         3051 ++$state->{_num_contains};
898              
899             last if $state->{short_circuit}
900             and (not exists $schema->{maxContains} or $state->{_num_contains} > $schema->{maxContains})
901 390 100 100     4125 and ($state->{_num_contains} >= ($schema->{minContains}//1));
      100        
      100        
      100        
902             }
903             }
904              
905             # note: no items contained is only valid when minContains is explicitly 0
906 504 100 66     7265 if (not $state->{_num_contains} and (($schema->{minContains}//1) > 0
      66        
907             or $state->{spec_version} and $state->{spec_version} eq 'draft7')) {
908 195         619 push $state->{errors}->@*, @errors;
909 195         612 return E($state, 'subschema is not valid against any item');
910             }
911              
912 309         1174 return 1;
913             }
914              
915 2401     2401   4267 sub _eval_keyword_properties ($data, $schema, $state) {
  2401         4440  
  2401         4117  
  2401         4267  
  2401         3589  
916 2401         7545 assert_keyword_type($state, $schema, 'object');
917 2401 100       5553 return 1 if not is_type('object', $data);
918              
919 2150         4542 my $valid = 1;
920 2150         11693 foreach my $property (sort keys $schema->{properties}->%*) {
921 2714 100       9067 next if not exists $data->{$property};
922              
923 1670 100       5561 if (is_type('boolean', $schema->{properties}{$property})) {
924 323 100       1998 next if $schema->{properties}{$property};
925 106         1489 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
926             _schema_path_suffix => $property }, 'property not permitted');
927             }
928             else {
929             next if _eval_subschema($data->{$property}, $schema->{properties}{$property},
930             +{ %$state,
931             data_path => jsonp($state->{data_path}, $property),
932 1347 100       9407 schema_path => jsonp($state->{schema_path}, 'properties', $property) });
933              
934 329         1561 $valid = 0;
935             }
936 435 100       2133 last if $state->{short_circuit};
937             }
938              
939 1995 100       8166 return E($state, 'not all properties are valid') if not $valid;
940 1582         4566 return 1;
941             }
942              
943 809     809   1392 sub _eval_keyword_patternProperties ($data, $schema, $state) {
  809         1512  
  809         1369  
  809         1435  
  809         1340  
944 809         2647 assert_keyword_type($state, $schema, 'object');
945              
946 809         4886 foreach my $property (sort keys $schema->{patternProperties}->%*) {
947 1250         11139 assert_pattern({ %$state, _schema_path_suffix => $property }, $property);
948             }
949              
950 807 100       2208 return 1 if not is_type('object', $data);
951              
952 614         1272 my $valid = 1;
953 614         2814 foreach my $property_pattern (sort keys $schema->{patternProperties}->%*) {
954 898         14474 foreach my $property (sort grep m/(?:$property_pattern)/, keys %$data) {
955 557 100       2408 if (is_type('boolean', $schema->{patternProperties}{$property_pattern})) {
956 319 100       2022 next if $schema->{patternProperties}{$property_pattern};
957 108         1457 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
958             _schema_path_suffix => $property_pattern }, 'property not permitted');
959             }
960             else {
961             next if _eval_subschema($data->{$property}, $schema->{patternProperties}{$property_pattern},
962             +{ %$state,
963             data_path => jsonp($state->{data_path}, $property),
964 238 100       1437 schema_path => jsonp($state->{schema_path}, 'patternProperties', $property_pattern) });
965              
966 87         404 $valid = 0;
967             }
968 195 100       1154 last if $state->{short_circuit};
969             }
970             }
971              
972 614 100       3978 return E($state, 'not all properties are valid') if not $valid;
973 434         1322 return 1;
974             }
975              
976 755     755   1353 sub _eval_keyword_additionalProperties ($data, $schema, $state) {
  755         1457  
  755         1276  
  755         1175  
  755         1183  
977 755 100       1922 return 1 if not is_type('object', $data);
978              
979 556         1138 my $valid = 1;
980 556         2296 foreach my $property (sort keys %$data) {
981 552 100 100     2645 next if exists $schema->{properties} and exists $schema->{properties}{$property};
982             next if exists $schema->{patternProperties}
983 438 100 100 148   2649 and any { $property =~ /(?:$_)/ } keys $schema->{patternProperties}->%*;
  148         2664  
984              
985 350 100       1243 if (is_type('boolean', $schema->{additionalProperties})) {
986 192 100       1099 next if $schema->{additionalProperties};
987              
988 172         2527 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
989             'additional property not permitted');
990             }
991             else {
992             next if _eval_subschema($data->{$property}, $schema->{additionalProperties},
993             +{ %$state,
994             data_path => jsonp($state->{data_path}, $property),
995 158 100       1138 schema_path => $state->{schema_path}.'/additionalProperties' });
996              
997 43         210 $valid = 0;
998             }
999 215 100       2018 last if $state->{short_circuit};
1000             }
1001              
1002 504 100       1742 return E($state, 'not all additional properties are valid') if not $valid;
1003 290         793 return 1;
1004             }
1005              
1006 463     463   867 sub _eval_keyword_propertyNames ($data, $schema, $state) {
  463         846  
  463         841  
  463         692  
  463         754  
1007 463 100       1242 return 1 if not is_type('object', $data);
1008              
1009 288         592 my $valid = 1;
1010 288         1220 foreach my $property (sort keys %$data) {
1011             next if _eval_subschema($property, $schema->{propertyNames},
1012             +{ %$state,
1013             data_path => jsonp($state->{data_path}, $property),
1014 202 100       1281 schema_path => $state->{schema_path}.'/propertyNames' });
1015              
1016 116         548 $valid = 0;
1017 116 100       458 last if $state->{short_circuit};
1018             }
1019              
1020 288 100       957 return E($state, 'not all property names are valid') if not $valid;
1021 172         501 return 1;
1022             }
1023              
1024 384     384   725 sub _eval_keyword_unevaluatedItems ($data, $schema, $state) {
  384         737  
  384         781  
  384         589  
  384         558  
1025 384         1083 abort($state, 'keyword not yet supported');
1026             }
1027              
1028 584     584   1223 sub _eval_keyword_unevaluatedProperties ($data, $schema, $state) {
  584         1215  
  584         1135  
  584         1082  
  584         1033  
1029 584         2154 abort($state, 'keyword not yet supported');
1030             }
1031              
1032             # UTILITIES
1033              
1034             # supports the six core types, plus integer (which is also a number)
1035             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1036             # note that sometimes a value may return true for more than one type, e.g. integer+number,
1037             # or number+string, depending on its internal flags.
1038             # copied from JSON::Schema::Modern::Utilities::is_type
1039 52894     52894 0 1424070 sub is_type ($type, $value) {
  52894         80846  
  52894         83062  
  52894         74691  
1040 52894 100       119281 if ($type eq 'null') {
1041 83         557 return !(defined $value);
1042             }
1043 52811 100       106124 if ($type eq 'boolean') {
1044 6022         15146 return is_bool($value);
1045             }
1046 46789 100       94840 if ($type eq 'object') {
1047 12088         46860 return is_plain_hashref($value);
1048             }
1049 34701 100       71756 if ($type eq 'array') {
1050 8790         34805 return is_plain_arrayref($value);
1051             }
1052              
1053 25911 100 100     105185 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
1054 25895 100       56655 return 0 if not defined $value;
1055 25877         145596 my $flags = B::svref_2object(\$value)->FLAGS;
1056              
1057             # dualvars with the same string and (stringified) numeric value could be either a string or a
1058             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
1059             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
1060             # numified strings do have POK set, so we can tell which one came first.
1061              
1062 25877 100       73575 if ($type eq 'string') {
1063             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
1064             return !is_ref($value)
1065             && $flags & B::SVf_POK
1066             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
1067 17   100 17   280873 || do { no warnings 'numeric'; 0+$value eq $value });
  17         49  
  17         11635  
  16845         134562  
1068             }
1069              
1070 9032 100       20815 if ($type eq 'number') {
1071             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1072 6143   100     13245 return is_bignum($value) || created_as_number($value);
1073             }
1074              
1075 2889 50       6736 if ($type eq 'integer') {
1076             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
1077             # therefore they will fail this check
1078 2889   100     6767 return is_bignum($value) && $value->is_int
1079             # if dualvar, PV and stringified NV/IV must be identical
1080             || created_as_number($value) && int($value) == $value;
1081             }
1082             }
1083              
1084 16 100       105 if ($type =~ /^reference to (.+)$/) {
1085 11   33     127 return !blessed($value) && ref($value) eq $1;
1086             }
1087              
1088 5         35 return ref($value) eq $type;
1089             }
1090              
1091             # returns one of the six core types, plus integer
1092             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1093             # copied from JSON::Schema::Modern::Utilities::get_type
1094 35088     35088 0 1202515 sub get_type ($value) {
  35088         61527  
  35088         48154  
1095 35088 100       101004 return 'object' if is_plain_hashref($value);
1096 13041 100       27240 return 'boolean' if is_bool($value);
1097 11062 100       33543 return 'null' if not defined $value;
1098 10764 100       24523 return 'array' if is_plain_arrayref($value);
1099              
1100             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1101 9481 100       19519 if (is_ref($value)) {
1102 455         1068 my $ref = ref($value);
1103 455 100       2639 return $ref eq 'Math::BigInt' ? 'integer'
    100          
    100          
    100          
1104             : $ref eq 'Math::BigFloat' ? ($value->is_int ? 'integer' : 'number')
1105             : (defined blessed($value) ? '' : 'reference to ').$ref;
1106             }
1107              
1108 9026         36425 my $flags = B::svref_2object(\$value)->FLAGS;
1109              
1110             # dualvars with the same string and (stringified) numeric value could be either a string or a
1111             # number, and before 5.36 we can't tell the difference, so we choose number because it has been
1112             # evaluated as a number already.
1113             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
1114             # numified strings do have POK set, so we can tell which one came first.
1115              
1116             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
1117             return 'string'
1118             if $flags & B::SVf_POK
1119             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
1120 17 100 100 17   152 || do { no warnings 'numeric'; 0+$value eq $value });
  17   100     40  
  17         3569  
  9026         38504  
1121              
1122             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
1123             # therefore they will fail this check
1124 4387 100       24641 return int($value) == $value ? 'integer' : 'number' if created_as_number($value);
    100          
1125              
1126             # this might be a scalar with POK|IOK or POK|NOK set
1127 15         100 return 'ambiguous type';
1128             }
1129              
1130             # lifted from JSON::MaybeXS
1131             # note: unlike builtin::compat::is_bool on older perls, we do not accept
1132             # dualvar(0,"") or dualvar(1,"1") because JSON::PP and Cpanel::JSON::XS
1133             # do not encode these as booleans.
1134 17     17   157 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  17         241  
  17         1964  
1135 17     17   175 use if HAVE_BUILTIN, experimental => 'builtin';
  17         111  
  17         633  
1136 19063     19063 0 27448 sub is_bool ($value) {
  19063         29266  
  19063         26500  
1137 19063 50 66     120955 HAVE_BUILTIN and builtin::is_bool($value)
      66        
1138             or
1139             !!blessed($value)
1140             and ($value->isa('JSON::PP::Boolean')
1141             or $value->isa('Cpanel::JSON::XS::Boolean')
1142             or $value->isa('JSON::XS::Boolean'));
1143             }
1144              
1145 11375     11375 0 17219 sub is_bignum ($value) {
  11375         17954  
  11375         16351  
1146 11375         80370 ref($value) =~ /^Math::Big(?:Int|Float)$/;
1147             }
1148              
1149             # compares two arbitrary data payloads for equality, as per
1150             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
1151             # $state hashref supports the following fields/configs:
1152             # - path: location of the first difference
1153             # - error: description of the difference
1154             # - $SCALARREF_BOOLEANS: treats \0 and \1 as boolean values
1155             # - $STRINGY_NUMBERS: strings will be typed as numbers if looks_like_number() is true
1156             # copied from JSON::Schema::Modern::Utilities::is_equal
1157 4171     4171 0 6948 sub is_equal ($x, $y, $state = {}) {
  4171         6650  
  4171         6849  
  4171         6693  
  4171         6240  
1158 4171   100     21093 $state->{path} //= '';
1159              
1160 4171         12408 my @types = map get_type($_), $x, $y;
1161              
1162 4171 100       16057 $state->{error} = 'ambiguous type encountered', return 0
1163             if grep $types[$_] eq 'ambiguous type', 0..1;
1164              
1165 4168 100       10425 if ($SCALARREF_BOOLEANS) {
1166 99 100       272 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
1167 99 100       287 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
1168             }
1169              
1170 4168 100       9166 if ($STRINGY_NUMBERS) {
1171 18 100 100     174 ($x, $types[0]) = (0+$x, int(0+$x) == $x ? 'integer' : 'number')
    100          
1172             if $types[0] eq 'string' and looks_like_number($x);
1173              
1174 18 100 100     124 ($y, $types[1]) = (0+$y, int(0+$y) == $y ? 'integer' : 'number')
    100          
1175             if $types[1] eq 'string' and looks_like_number($y);
1176             }
1177              
1178 4168 100       13520 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
1179 3325 100       7787 return 1 if $types[0] eq 'null';
1180 3311 100 100     15274 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
1181             if $types[0] eq 'string';
1182 1778 100 100     11344 ($x == $y and return 1), $state->{error} = "$types[0]s not equal", return 0
1183             if grep $types[0] eq $_, qw(boolean number integer);
1184              
1185 623         1550 my $path = $state->{path};
1186 623 100       1569 if ($types[0] eq 'object') {
1187 217 100       856 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
1188             if keys %$x != keys %$y;
1189              
1190 200 100       1502 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
1191 7         29 my $pos = substr($s->{path}, 1);
1192 7         34 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
1193 7         47 return 0;
1194             }
1195              
1196 193         804 foreach my $property (sort keys %$x) {
1197 231         672 $state->{path} = jsonp($path, $property);
1198 231 100       887 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
1199             }
1200              
1201 106         1040 return 1;
1202             }
1203              
1204 406 50       1007 if ($types[0] eq 'array') {
1205 406 100       1093 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
1206 397         1319 foreach my $idx (0 .. $x->$#*) {
1207 441         1596 $state->{path} = $path.'/'.$idx;
1208 441 100       1733 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
1209             }
1210 269         7548 return 1;
1211             }
1212              
1213 0         0 $state->{error} = 'uh oh', return 0; # should never get here
1214             }
1215              
1216             # checks array elements for uniqueness. short-circuits on first pair of matching elements
1217             # if second arrayref is provided, it is populated with the indices of identical items
1218             # supports the following configs:
1219             # - $SCALARREF_BOOLEANS: treats \0 and \1 as boolean values
1220             # - $STRINGY_NUMBERS: strings will be typed as numbers if looks_like_number() is true
1221             # copied from JSON::Schema::Modern::Utilities::is_elements_unique
1222 2358     2358 0 3864 sub is_elements_unique ($array, $equal_indices = undef) {
  2358         4242  
  2358         4246  
  2358         3530  
1223 2358         8589 foreach my $idx0 (0 .. $array->$#*-1) {
1224 846         2755 foreach my $idx1 ($idx0+1 .. $array->$#*) {
1225 1251 100       4465 if (is_equal($array->[$idx0], $array->[$idx1])) {
1226 207 50       3508 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
1227 207         784 return 0;
1228             }
1229             }
1230             }
1231 2151         7343 return 1;
1232             }
1233              
1234             # shorthand for creating and appending json pointers
1235             # the first argument is a json pointer; remaining arguments are path segments to be encoded and
1236             # appended
1237             # copied from JSON::Schema::Modern::Utilities::jsonp
1238             sub jsonp {
1239 24020     24020 0 228832 return join('/', shift, map s/~/~0/gr =~ s!/!~1!gr, grep defined, @_);
1240             }
1241              
1242             # shorthand for finding the canonical uri of the present schema location
1243             # copied from JSON::Schema::Modern::Utilities::canonical_uri
1244 30612     30612 0 48464 sub canonical_uri ($state, @extra_path) {
  30612         44481  
  30612         47428  
  30612         42519  
1245 30612 100 100     137307 return $state->{initial_schema_uri} if not @extra_path and not length($state->{schema_path});
1246 16601         74785 my $uri = $state->{initial_schema_uri}->clone;
1247 16601 100 100     1600608 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{schema_path}, @extra_path) : $state->{schema_path});
1248 16601 100       92815 undef $fragment if not length($fragment);
1249 16601         47901 $uri->fragment($fragment);
1250 16601         118274 $uri;
1251             }
1252              
1253             # shorthand for creating error objects
1254             # based on JSON::Schema::Modern::Utilities::E
1255 9843     9843 0 43378 sub E ($state, $error_string, @args) {
  9843         15725  
  9843         15704  
  9843         23391  
  9843         13256  
1256             # sometimes the keyword shouldn't be at the very end of the schema path
1257 9843         27477 my $sps = delete $state->{_schema_path_suffix};
1258 9843 100 100     56063 my @schema_path_suffix = defined $sps && is_plain_arrayref($sps) ? $sps->@* : $sps//();
      100        
1259              
1260 9843         28665 my $uri = canonical_uri($state, $state->{keyword}, @schema_path_suffix);
1261              
1262             my $keyword_location = $state->{traversed_schema_path}
1263 9843         35220 .jsonp($state->{schema_path}, $state->{keyword}, @schema_path_suffix);
1264              
1265 9843 100 100     34707 undef $uri if $uri eq '' and $keyword_location eq ''
      100        
      100        
      100        
1266             or ($uri->fragment//'') eq $keyword_location and $uri->clone->fragment(undef) eq '';
1267              
1268             push $state->{errors}->@*, {
1269             instanceLocation => $state->{data_path},
1270 9843 100       3844292 keywordLocation => $keyword_location,
    100          
1271             defined $uri ? ( absoluteKeywordLocation => $uri->to_string) : (),
1272             error => @args ? sprintf($error_string, @args) : $error_string,
1273             };
1274              
1275 9843         335952 return 0;
1276             }
1277              
1278             # creates an error object, but also aborts evaluation immediately
1279             # only this error is returned, because other errors on the stack might not actually be "real"
1280             # errors (consider if we were in the middle of evaluating a "not" or "if")
1281 1674     1674 0 326310 sub abort ($state, $error_string, @args) {
  1674         3058  
  1674         3241  
  1674         2923  
  1674         2692  
1282 1674         5994 E($state, $error_string, @args);
1283 1674         43366 die pop $state->{errors}->@*;
1284             }
1285              
1286             # one common usecase of abort()
1287 28018     28018 0 42478 sub assert_keyword_type ($state, $schema, $type) {
  28018         42322  
  28018         40701  
  28018         43847  
  28018         37555  
1288 28018 100       96350 return 1 if is_type($type, $schema->{$state->{keyword}});
1289 18 100       120 abort($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
1290             }
1291              
1292 2149     2149 0 3681 sub assert_pattern ($state, $pattern) {
  2149         3595  
  2149         3314  
  2149         3164  
1293 2149         4437 try {
1294 2149     0   16169 local $SIG{__WARN__} = sub { die @_ };
  0         0  
1295 2149         33875 qr/$pattern/;
1296             }
1297 3         9 catch ($e) { abort($state, $e); }
1298 2146         8928 return 1;
1299             }
1300              
1301             # based on JSON::Schema::Modern::Utilities::assert_uri_reference
1302 2421     2421 0 3881 sub assert_uri_reference ($state, $schema) {
  2421         3863  
  2421         3738  
  2421         3495  
1303 2421         6833 my $string = $schema->{$state->{keyword}};
1304             abort($state, '%s value is not a valid URI reference', $state->{keyword})
1305             # see also uri-reference format sub
1306 2421 50 33     10414 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      66        
      33        
1307             or $string =~ /[^[:ascii:]]/ # ascii characters only
1308             or $string =~ /#/ # no fragment, except...
1309             and $string !~ m{#$} # allow empty fragment
1310             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # allow plain-name fragment
1311             and $string !~ m{#/(?:[^~]|~[01])*$}; # allow json pointer fragment
1312              
1313 2421         983505 return 1;
1314             }
1315              
1316             # based on JSON::Schema::Modern::Utilities::assert_uri
1317 5648     5648 0 8607 sub assert_uri ($state, $schema, $override = undef) {
  5648         8361  
  5648         8106  
  5648         10113  
  5648         7912  
1318 5648   66     25661 my $string = $override // $schema->{$state->{keyword}};
1319 5648         22842 my $uri = Mojo::URL->new($string);
1320              
1321 5648 0 33     566815 abort($state, '"%s" is not a valid URI', $string)
      33        
      66        
      33        
      33        
      33        
1322             # see also uri format sub
1323             if fc($uri->to_unsafe_string) ne fc($string)
1324             or $string =~ /[^[:ascii:]]/ # ascii characters only
1325             or not $uri->is_abs # must have a schema
1326             or $string =~ /#/ # no fragment, except...
1327             and $string !~ m{#$} # empty fragment
1328             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
1329             and $string !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
1330              
1331 5648         1393163 return 1;
1332             }
1333              
1334 2788     2788 0 4815 sub assert_non_negative_integer ($schema, $state) {
  2788         4486  
  2788         6684  
  2788         4228  
1335 2788         8057 assert_keyword_type($state, $schema, 'integer');
1336             abort($state, '%s value is not a non-negative integer', $state->{keyword})
1337 2788 50       11709 if $schema->{$state->{keyword}} < 0;
1338 2788         48720 return 1;
1339             }
1340              
1341 1928     1928 0 3627 sub assert_array_schemas ($schema, $state) {
  1928         3283  
  1928         3067  
  1928         2998  
1342 1928         6309 assert_keyword_type($state, $schema, 'array');
1343 1928 50       7826 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
1344 1928         4155 return 1;
1345             }
1346              
1347             # copied from JSON::Schema::Modern::Utilities::sprintf_num
1348 989     989 0 3860 sub sprintf_num ($value) {
  989         1742  
  989         1543  
1349             # use original value as stored in the NV, without losing precision
1350 989 100       2105 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
1351             }
1352              
1353             1;
1354              
1355             __END__