File Coverage

blib/lib/JSON/Schema/Tiny.pm
Criterion Covered Total %
statement 920 925 99.4
branch 512 564 90.7
condition 344 425 80.9
subroutine 103 103 100.0
pod 1 19 5.2
total 1880 2036 92.3


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   4727926 use strict;
  17         29  
  17         532  
3 17     17   69 use warnings;
  17         42  
  17         985  
4 17     17   78 use if $ENV{AUTHOR_TESTING}, strictures => version => 2;
  17         28  
  17         950  
5             package JSON::Schema::Tiny; # git description: v0.032-2-g832c892
6             # vim: set ts=8 sts=2 sw=2 tw=100 et :
7             # ABSTRACT: Validate data against a schema, minimally
8             # KEYWORDS: JSON Schema data validation structure specification tiny
9              
10             our $VERSION = '0.033';
11              
12 17     17   223 use 5.020; # for unicode_strings, signatures, postderef features
  17         59  
13 17     17   59 use stable 0.031 'postderef';
  17         217  
  17         102  
14 17     17   2862 use experimental 0.026 qw(signatures args_array_with_signatures);
  17         192  
  17         67  
15 17     17   1335 use if $ENV{AUTHOR_TESTING}, autovivification => warn => qw(fetch store exists delete);
  17         21  
  17         793  
16 17     17   88 use if "$]" >= 5.022, experimental => 're_strict';
  17         56  
  17         285  
17 17     17   1071 no if "$]" >= 5.031009, feature => 'indirect';
  17         25  
  17         969  
18 17     17   92 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         45  
  17         857  
19 17     17   74 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         37  
  17         744  
20 17     17   70 use B;
  17         32  
  17         445  
21 17     17   7314 use Mojo::URL;
  17         1877608  
  17         97  
22 17     17   7535 use Mojo::JSON::Pointer;
  17         11815  
  17         89  
23 17     17   687 use Carp qw(croak carp);
  17         77  
  17         893  
24 17     17   4366 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  17         229689  
  17         503  
25 17     17   4934 use Feature::Compat::Try;
  17         3452  
  17         102  
26 17     17   1057 use JSON::PP ();
  17         27  
  17         587  
27 17     17   64 use if "$]" < 5.041010, 'List::Util' => 'any';
  17         19  
  17         754  
28 17     17   64 use if "$]" >= 5.041010, experimental => 'keyword_any';
  17         24  
  17         304  
29 17     17   1052 use Scalar::Util 'looks_like_number';
  17         22  
  17         889  
30 17     17   7020 use builtin::compat qw(blessed created_as_number);
  17         179901  
  17         89  
31 17     17   2749 use if "$]" >= 5.022, POSIX => 'isinf';
  17         26  
  17         7896  
32 17     17   142691 use Math::BigFloat;
  17         1158293  
  17         87  
33 17     17   390929 use namespace::clean;
  17         35  
  17         167  
34 17     17   4592 use Exporter 5.57 'import';
  17         269  
  17         16309  
35              
36             our @EXPORT_OK = qw(evaluate);
37              
38             our $BOOLEAN_RESULT = 0;
39             our $SHORT_CIRCUIT = 0;
40             our $MAX_TRAVERSAL_DEPTH = 50;
41             our $MOJO_BOOLEANS; # deprecated; renamed to $SCALARREF_BOOLEANS
42             our $SCALARREF_BOOLEANS;
43             our $STRINGY_NUMBERS;
44             our $SPECIFICATION_VERSION;
45              
46             my %version_uris = (
47             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12',
48             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09',
49             'http://json-schema.org/draft-07/schema#' => 'draft7',
50             );
51              
52 18     18 0 2854368 sub new ($class, %args) {
  18         31  
  18         42  
  18         24  
53 18         62 bless(\%args, $class);
54             }
55              
56             sub evaluate {
57 12777 50   12777 1 21778730 croak 'evaluate called in void context' if not defined wantarray;
58              
59 12777   66     45504 $SCALARREF_BOOLEANS = $SCALARREF_BOOLEANS // $MOJO_BOOLEANS;
60             local $BOOLEAN_RESULT = $_[0]->{boolean_result} // $BOOLEAN_RESULT,
61             local $SHORT_CIRCUIT = $_[0]->{short_circuit} // $SHORT_CIRCUIT,
62             local $MAX_TRAVERSAL_DEPTH = $_[0]->{max_traversal_depth} // $MAX_TRAVERSAL_DEPTH,
63             local $SCALARREF_BOOLEANS = $_[0]->{scalarref_booleans} // $SCALARREF_BOOLEANS // $_[0]->{mojo_booleans},
64             local $STRINGY_NUMBERS = $_[0]->{stringy_numbers} // $STRINGY_NUMBERS,
65 12777 100 33     187572 local $SPECIFICATION_VERSION = $_[0]->{specification_version} // $SPECIFICATION_VERSION,
      66        
      66        
      33        
      33        
      33        
      66        
      100        
66             shift
67             if blessed($_[0]) and blessed($_[0])->isa(__PACKAGE__);
68              
69 12777 100       25162 if (defined $SPECIFICATION_VERSION) {
70             $SPECIFICATION_VERSION = 'draft'.$SPECIFICATION_VERSION
71 12631 100 100     38877 if $SPECIFICATION_VERSION !~ /^draft/ and any { 'draft'.$SPECIFICATION_VERSION eq $_ } values %version_uris;
  9         26  
72              
73 12631 100       25368 croak '$SPECIFICATION_VERSION value is invalid' if not any { $SPECIFICATION_VERSION eq $_ } values %version_uris;
  29556         60773  
74             }
75              
76 12776 50       21860 croak 'insufficient arguments' if @_ < 2;
77 12776         20772 my ($data, $schema) = @_;
78              
79 12776   100     43187 my $state = {
80             depth => 0,
81             data_path => '',
82             traversed_schema_path => '', # the accumulated traversal path up to the last $ref traversal
83             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start or the last traversed $ref
84             schema_path => '', # the rest of the path, since the start or the last traversed $ref
85             errors => [],
86             seen => {},
87             short_circuit => $BOOLEAN_RESULT || $SHORT_CIRCUIT,
88             root_schema => $schema, # so we can do $refs within the same document
89             spec_version => $SPECIFICATION_VERSION,
90             };
91              
92 12776         182516 my $valid;
93 12776         20193 try {
94 12776         24614 $valid = _eval_subschema($data, $schema, $state)
95             }
96             catch ($e) {
97 1675 100       4116 if (ref $e eq 'HASH') {
98 1674         2973 push $state->{errors}->@*, $e;
99             }
100             else {
101 1         3 E($state, 'EXCEPTION: '.$e);
102             }
103              
104 1675         3662 $valid = 0;
105             }
106              
107 12776 50 66     34796 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
108              
109             return $BOOLEAN_RESULT ? $valid : +{
110             valid => $valid ? JSON::PP::true : JSON::PP::false,
111 12776 100       52701 $valid ? () : (errors => $state->{errors}),
    100          
    100          
112             };
113             }
114              
115             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
116              
117             # current spec version => { keyword => undef, or arrayref of alternatives }
118             my %removed_keywords = (
119             'draft7' => {
120             id => [ '$id' ],
121             },
122             'draft2019-09' => {
123             id => [ '$id' ],
124             definitions => [ '$defs' ],
125             dependencies => [ qw(dependentSchemas dependentRequired) ],
126             },
127             'draft2020-12' => {
128             id => [ '$id' ],
129             definitions => [ '$defs' ],
130             dependencies => [ qw(dependentSchemas dependentRequired) ],
131             '$recursiveAnchor' => [ '$dynamicAnchor' ],
132             '$recursiveRef' => [ '$dynamicRef' ],
133             additionalItems => [ 'items' ],
134             },
135             );
136              
137 21636     21636   27287 sub _eval_subschema ($data, $schema, $state) {
  21636         25282  
  21636         24657  
  21636         21993  
  21636         22282  
138 21636 50       33205 croak '_eval_subschema called in void context' if not defined wantarray;
139              
140             # do not propagate upwards changes to depth, traversed paths,
141             # but additions to errors are by reference and will be retained
142 21636         92437 $state = { %$state };
143 21636         115572 delete $state->@{'keyword', grep /^_/, keys %$state};
144              
145             abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
146 21636 100       53380 if $state->{depth}++ > $MAX_TRAVERSAL_DEPTH;
147              
148 21633         39883 my $schema_type = get_type($schema);
149 21633 100 66     38440 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
150 20882 100       33685 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
151              
152 20863 100       37443 return 1 if not keys %$schema;
153              
154             # find all schema locations in effect at this data path + canonical_uri combination
155             # if any of them are absolute prefix of this schema location, we are in a loop.
156 20504         34998 my $canonical_uri = canonical_uri($state);
157 20504         35907 my $schema_location = $state->{traversed_schema_path}.$state->{schema_path};
158             {
159 17     17   5290 use autovivification qw(fetch store);
  17         9021  
  17         88  
  20504         22519  
160             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
161             if grep substr($schema_location, 0, length) eq $_,
162 20504 100       70223 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
163 20502         2823327 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
164             }
165              
166 20502         1917336 my $valid = 1;
167 20502   100     44521 my $spec_version = $state->{spec_version}//'';
168              
169 20502 100 100     547159 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        
170             # CORE KEYWORDS
171             qw($id $schema),
172             !$spec_version || $spec_version ne 'draft7' ? '$anchor' : (),
173             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveAnchor' : (),
174             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicAnchor' : (),
175             '$ref',
176             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveRef' : (),
177             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicRef' : (),
178             !$spec_version || $spec_version ne 'draft7' ? '$vocabulary' : (),
179             '$comment',
180             !$spec_version || $spec_version eq 'draft7' ? 'definitions' : (),
181             !$spec_version || $spec_version ne 'draft7' ? '$defs' : (),
182             # APPLICATOR KEYWORDS
183             qw(allOf anyOf oneOf not if),
184             !$spec_version || $spec_version ne 'draft7' ? 'dependentSchemas' : (),
185             !$spec_version || $spec_version eq 'draft7' ? 'dependencies' : (),
186             !$spec_version || $spec_version !~ qr/^draft(?:7|2019-09)$/ ? 'prefixItems' : (),
187             'items',
188             !$spec_version || $spec_version =~ qr/^draft(?:7|2019-09)$/ ? 'additionalItems' : (),
189             qw(contains properties patternProperties additionalProperties propertyNames),
190             # UNEVALUATED KEYWORDS
191             !$spec_version || $spec_version ne 'draft7' ? qw(unevaluatedItems unevaluatedProperties) : (),
192             # VALIDATOR KEYWORDS
193             qw(type enum const
194             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
195             maxLength minLength pattern
196             maxItems minItems uniqueItems),
197             !$spec_version || $spec_version ne 'draft7' ? qw(maxContains minContains) : (),
198             qw(maxProperties minProperties required),
199             !$spec_version || $spec_version ne 'draft7' ? 'dependentRequired' : (),
200             ) {
201 720298 100       1023434 next if not exists $schema->{$keyword};
202              
203             # keywords adjacent to $ref (except for definitions) are not evaluated before draft2019-09
204             next if $keyword ne '$ref' and $keyword ne 'definitions'
205 34296 100 100     122766 and exists $schema->{'$ref'} and $spec_version eq 'draft7';
      100        
      100        
206              
207 34281         70467 $state->{keyword} = $keyword;
208 34281         47643 my $error_count = $state->{errors}->@*;
209              
210 34281         185569 my $sub = __PACKAGE__->can('_eval_keyword_'.($keyword =~ s/^\$//r));
211 34281 100       71265 if (not $sub->($data, $schema, $state)) {
212             warn 'result is false but there are no errors (keyword: '.$keyword.')'
213 7501 50       15057 if $error_count == $state->{errors}->@*;
214 7501         9273 $valid = 0;
215             }
216              
217 31682 100 100     169812 last if not $valid and $state->{short_circuit};
218             }
219              
220             # check for previously-supported but now removed keywords
221 17903   100     100956 foreach my $keyword (sort keys(($removed_keywords{$spec_version}//{})->%*)) {
222 62636 100       91902 next if not exists $schema->{$keyword};
223 214         450 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
224             .canonical_uri($state).'")';
225 214 50       19109 if (my $alternates = $removed_keywords{$spec_version}->{$keyword}) {
226 214         781 my @list = map '"'.$_.'"', @$alternates;
227 214 50       500 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
228 214 100       538 splice(@list, -1, 0, 'or') if @list > 1;
229 214         545 $message .= ': this should be rewritten as '.join(' ', @list);
230             }
231 214         23853 carp $message;
232             }
233              
234 17903         111554 return $valid;
235             }
236              
237             # KEYWORD IMPLEMENTATIONS
238              
239 5644     5644   6584 sub _eval_keyword_schema ($data, $schema, $state) {
  5644         7222  
  5644         6705  
  5644         6182  
  5644         5659  
240 5644         12820 assert_keyword_type($state, $schema, 'string');
241 5644         14181 assert_uri($state, $schema);
242              
243             return abort($state, '$schema can only appear at the schema resource root')
244 5644 100       12290 if length($state->{schema_path});
245              
246 5643         14044 my $spec_version = $version_uris{$schema->{'$schema'}};
247 5643 100       10132 abort($state, 'custom $schema URIs are not supported (must be one of: %s',
248             join(', ', map '"'.$_.'"', keys %version_uris))
249             if not $spec_version;
250              
251 5614 100 100     15616 abort($state, '"$schema" indicates a different version than that requested by $JSON::Schema::Tiny::SPECIFICATION_VERSION')
252             if defined $SPECIFICATION_VERSION and $SPECIFICATION_VERSION ne $spec_version;
253              
254             # we special-case this because the check in _eval for older drafts + $ref has already happened
255             abort($state, '$schema and $ref cannot be used together in older drafts')
256 5613 100 100     12574 if exists $schema->{'$ref'} and $spec_version eq 'draft7';
257              
258 5612         14532 $state->{spec_version} = $spec_version;
259             }
260              
261 1699     1699   2235 sub _eval_keyword_ref ($data, $schema, $state) {
  1699         2143  
  1699         1980  
  1699         1884  
  1699         1830  
262 1699         3593 assert_keyword_type($state, $schema, 'string');
263 1699         3893 assert_uri_reference($state, $schema);
264              
265 1699         5487 my $uri = Mojo::URL->new($schema->{$state->{keyword}})->to_abs($state->{initial_schema_uri});
266             abort($state, '%ss to anchors are not supported', $state->{keyword})
267 1699 100 100     537190 if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
268              
269             # the base of the $ref uri must be the same as the base of the root schema
270             # unfortunately this means that many uses of $ref won't work, because we don't
271             # track the locations of $ids in this or other documents.
272             abort($state, 'only same-document, same-base JSON pointers are supported in %s', $state->{keyword})
273 1601 100 100     17231 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
274              
275 1132   100     458183 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment//'');
276 1132 100       39898 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
277              
278             return _eval_subschema($data, $subschema,
279             +{ %$state,
280             traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/'.$state->{keyword},
281 1127         11107 initial_schema_uri => $uri,
282             schema_path => '',
283             });
284             }
285              
286 52     52   106 sub _eval_keyword_recursiveRef ($data, $schema, $state) {
  52         89  
  52         55  
  52         53  
  52         71  
287 52         145 assert_keyword_type($state, $schema, 'string');
288 52         123 assert_uri_reference($state, $schema);
289              
290 52         162 my $uri = Mojo::URL->new($schema->{'$recursiveRef'})->to_abs($state->{initial_schema_uri});
291 52 50 50     16037 abort($state, '$recursiveRefs to anchors are not supported')
292             if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
293              
294             # the base of the $recursiveRef uri must be the same as the base of the root schema.
295             # unfortunately this means that nearly all usecases of $recursiveRef won't work, because we don't
296             # track the locations of $ids in this or other documents.
297             abort($state, 'only same-document, same-base JSON pointers are supported in $recursiveRef')
298 52 100 100     447 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
299              
300 8         2861 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
301 8 50       145 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
302              
303 8 50 33     24 if (is_type('boolean', $subschema->{'$recursiveAnchor'}) and $subschema->{'$recursiveAnchor'}) {
304             $uri = Mojo::URL->new($schema->{'$recursiveRef'})
305 0   0     0 ->to_abs($state->{recursive_anchor_uri} // $state->{initial_schema_uri});
306 0         0 $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
307 0 0       0 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
308             }
309              
310             return _eval_subschema($data, $subschema,
311             +{ %$state,
312 8         76 traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/$recursiveRef',
313             initial_schema_uri => $uri,
314             schema_path => '',
315             });
316             }
317              
318 12     12   31 sub _eval_keyword_dynamicRef { goto \&_eval_keyword_ref }
319              
320 670     670   863 sub _eval_keyword_id ($data, $schema, $state) {
  670         962  
  670         868  
  670         699  
  670         785  
321 670         1494 assert_keyword_type($state, $schema, 'string');
322 670         1642 assert_uri_reference($state, $schema);
323              
324 670         1865 my $uri = Mojo::URL->new($schema->{'$id'});
325              
326 670 100 100     53601 if (($state->{spec_version}//'') eq 'draft7') {
327 133 100       242 if (length($uri->fragment)) {
328 3 50       17 abort($state, '$id cannot change the base uri at the same time as declaring an anchor')
329             if length($uri->clone->fragment(undef));
330              
331 3 100       361 abort($state, '$id value does not match required syntax')
332             if $uri->fragment !~ m/^[A-Za-z][A-Za-z0-9_:.-]*$/;
333              
334 2         35 return 1;
335             }
336             }
337             else {
338 537 100       1087 abort($state, '$id value "%s" cannot have a non-empty fragment', $uri) if length $uri->fragment;
339             }
340              
341 665         3081 $uri->fragment(undef);
342 665 100       3329 return E($state, '$id cannot be empty') if not length $uri;
343              
344 641 100       80112 $state->{initial_schema_uri} = $uri->is_abs ? $uri : $uri->to_abs($state->{initial_schema_uri});
345 641         39692 $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path};
346 641         950 $state->{schema_path} = '';
347              
348 641         1536 return 1;
349             }
350              
351 12     12   12 sub _eval_keyword_anchor ($data, $schema, $state) {
  12         16  
  12         25  
  12         11  
  12         13  
352 12         26 assert_keyword_type($state, $schema, 'string');
353              
354             return 1 if
355             (!$state->{spec_version} or $state->{spec_version} eq 'draft2019-09')
356             and ($schema->{'$anchor'}//'') =~ /^[A-Za-z][A-Za-z0-9_:.-]*$/
357             or
358             (!$state->{spec_version} or $state->{spec_version} eq 'draft2020-12')
359 12 50 66     115 and ($schema->{'$anchor'}//'') =~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
      50        
      66        
      33        
      50        
      33        
      66        
360              
361 0         0 abort($state, '$anchor value does not match required syntax');
362             }
363              
364 92     92   121 sub _eval_keyword_recursiveAnchor ($data, $schema, $state) {
  92         123  
  92         110  
  92         96  
  92         99  
365 92         226 assert_keyword_type($state, $schema, 'boolean');
366 92 100 100     449 return 1 if not $schema->{'$recursiveAnchor'} or exists $state->{recursive_anchor_uri};
367              
368             # this is required because the location is used as the base URI for future resolution
369             # of $recursiveRef, and the fragment would be disregarded in the base
370             abort($state, '"$recursiveAnchor" keyword used without "$id"')
371 51 50       357 if not exists $schema->{'$id'};
372              
373             # record the canonical location of the current position, to be used against future resolution
374             # of a $recursiveRef uri -- as if it was the current location when we encounter a $ref.
375 51         95 $state->{recursive_anchor_uri} = canonical_uri($state);
376              
377 51         96 return 1;
378             }
379              
380 10     10   15 sub _eval_keyword_dynamicAnchor ($data, $schema, $state) {
  10         15  
  10         13  
  10         14  
  10         13  
381 10 50       20 return if not assert_keyword_type($state, $schema, 'string');
382              
383             abort($state, '$dynamicAnchor value does not match required syntax')
384 10 50       54 if $schema->{'$dynamicAnchor'} !~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
385 10         22 return 1;
386             }
387              
388 4     4   6 sub _eval_keyword_vocabulary ($data, $schema, $state) {
  4         7  
  4         7  
  4         7  
  4         5  
389 4         13 assert_keyword_type($state, $schema, 'object');
390              
391 4         15 foreach my $uri (sort keys $schema->{'$vocabulary'}->%*) {
392             abort({ %$state, _schema_path_suffix => $uri }, '$vocabulary value at "%s" is not a boolean', $uri)
393 4 50       19 if not is_type('boolean', $schema->{'$vocabulary'}{$uri});
394              
395 4         10 assert_uri($state, undef, $uri);
396             }
397              
398             abort($state, '$vocabulary can only appear at the schema resource root')
399 4 50       12 if length($state->{schema_path});
400              
401             abort($state, '$vocabulary can only appear at the document root')
402 4 50       11 if length($state->{traversed_schema_path}.$state->{schema_path});
403              
404 4         10 return 1;
405             }
406              
407 288     288   363 sub _eval_keyword_comment ($data, $schema, $state) {
  288         388  
  288         323  
  288         285  
  288         275  
408 288         629 assert_keyword_type($state, $schema, 'string');
409 288         571 return 1;
410             }
411              
412 150     150   475 sub _eval_keyword_definitions { goto \&_eval_keyword_defs }
413              
414 677     677   1041 sub _eval_keyword_defs ($data, $schema, $state) {
  677         987  
  677         784  
  677         795  
  677         755  
415 677         1548 assert_keyword_type($state, $schema, 'object');
416 675         1443 return 1;
417             }
418              
419 3706     3706   4696 sub _eval_keyword_type ($data, $schema, $state) {
  3706         5079  
  3706         4264  
  3706         3995  
  3706         4051  
420 3706 100       7296 if (ref $schema->{type} eq 'ARRAY') {
421 163 50       328 abort($state, 'type array is empty') if not $schema->{type}->@*;
422 163         310 foreach my $type ($schema->{type}->@*) {
423             abort($state, 'unrecognized type "%s"', $type//'')
424 344 100 50     427 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  1397   100     2398  
425             }
426 157 50       336 abort($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
427              
428 157         274 my $type = get_type($data);
429             return 1 if any {
430 292 100 100     1940 $type eq $_ or ($_ eq 'number' and $type eq 'integer')
      100        
      66        
      66        
      100        
      66        
      66        
      100        
      100        
431             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
432             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
433             or ($_ eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR')
434 157 100       418 } $schema->{type}->@*;
435 92         271 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
436             }
437             else {
438 3543         7808 assert_keyword_type($state, $schema, 'string');
439             abort($state, 'unrecognized type "%s"', $schema->{type}//'')
440 3537 100 50     6338 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  16514   50     31716  
441              
442 3535         7488 my $type = get_type($data);
443             return 1 if $type eq $schema->{type} or ($schema->{type} eq 'number' and $type eq 'integer')
444             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
445             and ($schema->{type} eq 'number' or ($schema->{type} eq 'integer' and $data == int($data))))
446 3535 100 100     17217 or ($schema->{type} eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR');
      100        
      100        
      66        
      66        
      66        
      66        
      100        
      100        
      66        
447 985         2158 return E($state, 'got %s, not %s', $type, $schema->{type});
448             }
449             }
450              
451 483     483   648 sub _eval_keyword_enum ($data, $schema, $state) {
  483         720  
  483         555  
  483         529  
  483         499  
452 483         1067 assert_keyword_type($state, $schema, 'array');
453              
454 483         639 my @s; my $idx = 0;
  483         692  
455 483 100       900 return 1 if any { is_equal($data, $_, $s[$idx++] = {}) } $schema->{enum}->@*;
  933         10800  
456              
457             return E($state, 'value does not match'
458 219 100       1176 .(!(grep $_->{path}, @s) ? ''
459             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
460             }
461              
462 1055     1055   1447 sub _eval_keyword_const ($data, $schema, $state) {
  1055         1484  
  1055         1257  
  1055         1167  
  1055         1180  
463 1055 100       2583 return 1 if is_equal($data, $schema->{const}, my $s = {});
464 479 100       6217 return E($state, 'value does not match'.($s->{path} ? " (at '$s->{path}': $s->{error})" : ''));
465             }
466              
467 832     832   1081 sub _eval_keyword_multipleOf ($data, $schema, $state) {
  832         1136  
  832         963  
  832         937  
  832         893  
468 832         1868 assert_keyword_type($state, $schema, 'number');
469 830 50       1827 abort($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
470              
471             return 1 if not is_type('number', $data)
472             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data)
473 830 50 66     18445 and do { $data = 0+$data; 1 });
  2   66     10  
  2   33     6  
      100        
474              
475             # if either value is a float, use the bignum library for the calculation
476 640 100 100     1045 if (is_bignum($data) or is_bignum($schema->{multipleOf})
      66        
      100        
477             or get_type($data) eq 'number' or get_type($schema->{multipleOf}) eq 'number') {
478 52 100       128 $data = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
479 52 100       3184 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
480 52         1339 my ($quotient, $remainder) = $data->bdiv($divisor);
481 52 50       56787 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
482 52 100       427 return 1 if $remainder == 0;
483             }
484             else {
485 588         1165 my $quotient = $data / $schema->{multipleOf};
486 588 50       3991 return E($state, 'overflow while calculating quotient')
    50          
487             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
488 588 100       1831 return 1 if int($quotient) == $quotient;
489             }
490              
491 297         7356 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
492             }
493              
494 585     585   733 sub _eval_keyword_maximum ($data, $schema, $state) {
  585         739  
  585         694  
  585         651  
  585         666  
495 585         1316 assert_keyword_type($state, $schema, 'number');
496 583 50 66     953 return 1 if not is_type('number', $data)
      66        
      100        
497             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
498 388 100       1052 return 1 if 0+$data <= $schema->{maximum};
499 172         8105 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
500             }
501              
502 483     483   601 sub _eval_keyword_exclusiveMaximum ($data, $schema, $state) {
  483         640  
  483         571  
  483         568  
  483         548  
503 483         1004 assert_keyword_type($state, $schema, 'number');
504 481 50 66     886 return 1 if not is_type('number', $data)
      66        
      100        
505             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
506 292 100       834 return 1 if 0+$data < $schema->{exclusiveMaximum};
507 154         10766 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
508             }
509              
510 713     713   936 sub _eval_keyword_minimum ($data, $schema, $state) {
  713         953  
  713         835  
  713         1682  
  713         754  
511 713         1583 assert_keyword_type($state, $schema, 'number');
512 711 50 66     1203 return 1 if not is_type('number', $data)
      66        
      100        
513             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
514 507 100       1389 return 1 if 0+$data >= $schema->{minimum};
515 242         19991 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
516             }
517              
518 423     423   484 sub _eval_keyword_exclusiveMinimum ($data, $schema, $state) {
  423         551  
  423         487  
  423         437  
  423         462  
519 423         905 assert_keyword_type($state, $schema, 'number');
520 421 50 66     706 return 1 if not is_type('number', $data)
      66        
      100        
521             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
522 232 100       653 return 1 if 0+$data > $schema->{exclusiveMinimum};
523 124         9633 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
524             }
525              
526 561     561   626 sub _eval_keyword_maxLength ($data, $schema, $state) {
  561         718  
  561         676  
  561         604  
  561         617  
527 561         1224 assert_non_negative_integer($schema, $state);
528              
529 561 100       822 return 1 if not is_type('string', $data);
530 352 100       910 return 1 if length($data) <= $schema->{maxLength};
531 162         1866 return E($state, 'length is greater than %d', $schema->{maxLength});
532             }
533              
534 512     512   650 sub _eval_keyword_minLength ($data, $schema, $state) {
  512         747  
  512         562  
  512         580  
  512         542  
535 512         1124 assert_non_negative_integer($schema, $state);
536              
537 512 100       771 return 1 if not is_type('string', $data);
538 302 100       852 return 1 if length($data) >= $schema->{minLength};
539 142         1833 return E($state, 'length is less than %d', $schema->{minLength});
540             }
541              
542 899     899   1190 sub _eval_keyword_pattern ($data, $schema, $state) {
  899         1240  
  899         1077  
  899         1011  
  899         1032  
543 899         1891 assert_keyword_type($state, $schema, 'string');
544 899         2275 assert_pattern($state, $schema->{pattern});
545              
546 898 100       1424 return 1 if not is_type('string', $data);
547 671 100       5045 return 1 if $data =~ m/(?:$schema->{pattern})/;
548 313         726 return E($state, 'pattern does not match');
549             }
550              
551 425     425   533 sub _eval_keyword_maxItems ($data, $schema, $state) {
  425         505  
  425         469  
  425         449  
  425         441  
552 425         974 assert_non_negative_integer($schema, $state);
553              
554 425 100       663 return 1 if not is_type('array', $data);
555 256 100       650 return 1 if @$data <= $schema->{maxItems};
556 122 100       1727 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
557             }
558              
559 424     424   459 sub _eval_keyword_minItems ($data, $schema, $state) {
  424         502  
  424         470  
  424         433  
  424         404  
560 424         909 assert_non_negative_integer($schema, $state);
561              
562 424 100       592 return 1 if not is_type('array', $data);
563 257 100       650 return 1 if @$data >= $schema->{minItems};
564 124 100       1579 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
565             }
566              
567 775     775   971 sub _eval_keyword_uniqueItems ($data, $schema, $state) {
  775         887  
  775         907  
  775         812  
  775         735  
568 775         1522 assert_keyword_type($state, $schema, 'boolean');
569 775 100       1267 return 1 if not is_type('array', $data);
570 614 100       2223 return 1 if not $schema->{uniqueItems};
571 449 100       3303 return 1 if is_elements_unique($data, my $equal_indices = []);
572 207         431 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
573             }
574              
575 84     84   101 sub _eval_keyword_maxContains ($data, $schema, $state) {
  84         95  
  84         89  
  84         86  
  84         79  
576 84         194 assert_non_negative_integer($schema, $state);
577 84 100       179 return 1 if not exists $state->{_num_contains};
578 76 50       175 return 1 if not is_type('array', $data);
579              
580             return E($state, 'contains too many matching items')
581 76 100       182 if $state->{_num_contains} > $schema->{maxContains};
582              
583 44         1279 return 1;
584             }
585              
586 102     102   129 sub _eval_keyword_minContains ($data, $schema, $state) {
  102         120  
  102         183  
  102         106  
  102         100  
587 102         208 assert_non_negative_integer($schema, $state);
588 102 100       224 return 1 if not exists $state->{_num_contains};
589 94 50       135 return 1 if not is_type('array', $data);
590              
591             return E($state, 'contains too few matching items')
592 94 100       197 if $state->{_num_contains} < $schema->{minContains};
593              
594 60         1207 return 1;
595             }
596              
597 340     340   393 sub _eval_keyword_maxProperties ($data, $schema, $state) {
  340         380  
  340         393  
  340         360  
  340         401  
598 340         748 assert_non_negative_integer($schema, $state);
599              
600 340 100       467 return 1 if not is_type('object', $data);
601 202 100       552 return 1 if keys %$data <= $schema->{maxProperties};
602             return E($state, 'more than %d propert%s', $schema->{maxProperties},
603 98 100       1820 $schema->{maxProperties} > 1 ? 'ies' : 'y');
604             }
605              
606 340     340   482 sub _eval_keyword_minProperties ($data, $schema, $state) {
  340         393  
  340         427  
  340         367  
  340         367  
607 340         718 assert_non_negative_integer($schema, $state);
608              
609 340 100       525 return 1 if not is_type('object', $data);
610 202 100       675 return 1 if keys %$data >= $schema->{minProperties};
611             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
612 98 100       1622 $schema->{minProperties} > 1 ? 'ies' : 'y');
613             }
614              
615 1414     1414   1770 sub _eval_keyword_required ($data, $schema, $state) {
  1414         1717  
  1414         1568  
  1414         1628  
  1414         1544  
616 1414         2781 assert_keyword_type($state, $schema, 'array');
617             abort($state, '"required" element is not a string')
618 1414 50       2509 if any { !is_type('string', $_) } $schema->{required}->@*;
  1600         2629  
619 1414 50       2892 abort($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
620              
621 1414 100       2176 return 1 if not is_type('object', $data);
622              
623 1264         3431 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
624 1264 100       2746 return 1 if not @missing;
625 566 100       2077 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
626             }
627              
628 271     271   410 sub _eval_keyword_dependentRequired ($data, $schema, $state) {
  271         360  
  271         355  
  271         312  
  271         286  
629 271         615 assert_keyword_type($state, $schema, 'object');
630              
631 271         825 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
632             E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
633 287 50       520 if not is_type('array', $schema->{dependentRequired}{$property});
634              
635 287         783 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
636             abort({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
637 301 100       620 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
638             }
639              
640             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
641 286 50       660 if not is_elements_unique($schema->{dependentRequired}{$property});
642             }
643              
644 270 100       454 return 1 if not is_type('object', $data);
645              
646 173         240 my $valid = 1;
647 173         364 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
648 189 100       418 next if not exists $data->{$property};
649              
650 153 100       656 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
651 79 100       830 $valid = E({ %$state, _schema_path_suffix => $property },
652             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
653             }
654             }
655              
656 173 100       439 return 1 if $valid;
657 79         170 return E($state, 'not all dependencies are satisfied');
658             }
659              
660 575     575   807 sub _eval_keyword_allOf ($data, $schema, $state) {
  575         821  
  575         800  
  575         719  
  575         653  
661 575         1525 assert_array_schemas($schema, $state);
662              
663 575         701 my @invalid;
664 575         1729 foreach my $idx (0..$schema->{allOf}->$#*) {
665             next if _eval_subschema($data, $schema->{allOf}[$idx],
666 843 100       7882 +{ %$state, schema_path => $state->{schema_path}.'/allOf/'.$idx });
667              
668 209         863 push @invalid, $idx;
669 209 100       559 last if $state->{short_circuit};
670             }
671              
672 404 100       1420 return 1 if @invalid == 0;
673              
674 169         307 my $pl = @invalid > 1;
675 169 100       718 return E($state, 'subschema%s %s %s not valid', $pl?'s':'', join(', ', @invalid), $pl?'are':'is');
    100          
676             }
677              
678 433     433   628 sub _eval_keyword_anyOf ($data, $schema, $state) {
  433         598  
  433         505  
  433         524  
  433         485  
679 433         1083 assert_array_schemas($schema, $state);
680              
681 433         562 my $valid = 0;
682 433         522 my @errors;
683 433         1191 foreach my $idx (0..$schema->{anyOf}->$#*) {
684             next if not _eval_subschema($data, $schema->{anyOf}[$idx],
685 760 100       6772 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/anyOf/'.$idx });
686 239         1092 ++$valid;
687 239 100       563 last if $state->{short_circuit};
688             }
689              
690 294 100       882 return 1 if $valid;
691 92         242 push $state->{errors}->@*, @errors;
692 92         212 return E($state, 'no subschemas are valid');
693             }
694              
695 509     509   614 sub _eval_keyword_oneOf ($data, $schema, $state) {
  509         611  
  509         589  
  509         550  
  509         530  
696 509         1279 assert_array_schemas($schema, $state);
697              
698 509         1814 my (@valid, @errors);
699 509         1377 foreach my $idx (0..$schema->{oneOf}->$#*) {
700             next if not _eval_subschema($data, $schema->{oneOf}[$idx],
701 1061 100       9134 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/oneOf/'.$idx });
702 377         1786 push @valid, $idx;
703 377 100 100     1152 last if @valid > 1 and $state->{short_circuit};
704             }
705              
706 358 100       1107 return 1 if @valid == 1;
707              
708 201 100       382 if (not @valid) {
709 123         300 push $state->{errors}->@*, @errors;
710 123         250 return E($state, 'no subschemas are valid');
711             }
712             else {
713 78         279 return E($state, 'multiple subschemas are valid: '.join(', ', @valid));
714             }
715             }
716              
717 293     293   377 sub _eval_keyword_not ($data, $schema, $state) {
  293         454  
  293         325  
  293         325  
  293         355  
718 293 100 66     624 return !$schema->{not} || E($state, 'subschema is true') if is_type('boolean', $schema->{not});
719              
720             return 1 if not _eval_subschema($data, $schema->{not},
721 181 100       1657 +{ %$state, schema_path => $state->{schema_path}.'/not', short_circuit => 1, errors => [] });
722              
723 135         535 return E($state, 'subschema is valid');
724             }
725              
726 326     326   481 sub _eval_keyword_if ($data, $schema, $state) {
  326         475  
  326         407  
  326         372  
  326         369  
727 326 100 100     918 return 1 if not exists $schema->{then} and not exists $schema->{else};
728             my $keyword = _eval_subschema($data, $schema->{if},
729 282 100       2398 +{ %$state, schema_path => $state->{schema_path}.'/if', short_circuit => 1, errors => [] })
730             ? 'then' : 'else';
731              
732 282 100       1607 return 1 if not exists $schema->{$keyword};
733              
734             return $schema->{$keyword} || E({ %$state, keyword => $keyword }, 'subschema is false')
735 224 100 66     468 if is_type('boolean', $schema->{$keyword});
736              
737             return 1 if _eval_subschema($data, $schema->{$keyword},
738 192 100       1423 +{ %$state, schema_path => $state->{schema_path}.'/'.$keyword });
739 62         537 return E({ %$state, keyword => $keyword }, 'subschema is not valid');
740             }
741              
742 337     337   494 sub _eval_keyword_dependentSchemas ($data, $schema, $state) {
  337         463  
  337         405  
  337         403  
  337         427  
743 337         721 assert_keyword_type($state, $schema, 'object');
744              
745 337 100       583 return 1 if not is_type('object', $data);
746              
747 213         339 my $valid = 1;
748 213         702 foreach my $property (sort keys $schema->{dependentSchemas}->%*) {
749             next if not exists $data->{$property}
750             or _eval_subschema($data, $schema->{dependentSchemas}{$property},
751 263 100 100     1218 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependentSchemas', $property) });
752              
753 97         382 $valid = 0;
754 97 100       273 last if $state->{short_circuit};
755             }
756              
757 213 100       683 return E($state, 'not all dependencies are satisfied') if not $valid;
758 116         248 return 1;
759             }
760              
761 186     186   240 sub _eval_keyword_dependencies ($data, $schema, $state) {
  186         236  
  186         213  
  186         203  
  186         228  
762 186         429 assert_keyword_type($state, $schema, 'object');
763              
764 186 100       327 return 1 if not is_type('object', $data);
765              
766 119         160 my $valid = 1;
767 119         335 foreach my $property (sort keys $schema->{dependencies}->%*) {
768 166 100       289 if (is_type('array', $schema->{dependencies}{$property})) {
769             # as in dependentRequired
770              
771 52         128 foreach my $index (0..$schema->{dependencies}{$property}->$#*) {
772             $valid = E({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
773 62 50       145 if not is_type('string', $schema->{dependencies}{$property}[$index]);
774             }
775              
776             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
777 52 50       129 if not is_elements_unique($schema->{dependencies}{$property});
778              
779 52 100       132 next if not exists $data->{$property};
780              
781 24 100       94 if (my @missing = grep !exists($data->{$_}), $schema->{dependencies}{$property}->@*) {
782 14 100       174 $valid = E({ %$state, _schema_path_suffix => $property },
783             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
784             }
785             }
786             else {
787             # as in dependentSchemas
788             next if not exists $data->{$property}
789             or _eval_subschema($data, $schema->{dependencies}{$property},
790 114 100 100     456 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependencies', $property) });
791              
792 47         191 $valid = 0;
793 47 100       137 last if $state->{short_circuit};
794             }
795             }
796              
797 119 100       333 return 1 if $valid;
798 59         103 return E($state, 'not all dependencies are satisfied');
799             }
800              
801 411     411   471 sub _eval_keyword_prefixItems ($data, $schema, $state) {
  411         528  
  411         499  
  411         470  
  411         444  
802 411 50       849 return if not assert_array_schemas($schema, $state);
803 411         1194 goto \&_eval_keyword__items_array_schemas;
804             }
805              
806 1304     1304   1650 sub _eval_keyword_items ($data, $schema, $state) {
  1304         1686  
  1304         1480  
  1304         1311  
  1304         1518  
807 1304 100       3252 if (ref $schema->{items} eq 'ARRAY') {
808             abort($state, 'array form of "items" not supported in %s', $state->{spec_version})
809 700 100 100     1601 if ($state->{spec_version}//'') eq 'draft2020-12';
810              
811 699         2113 goto \&_eval_keyword__items_array_schemas;
812             }
813              
814 604   100     2149 $state->{_last_items_index} //= -1;
815 604         1785 goto \&_eval_keyword__items_schema;
816             }
817              
818 219     219   266 sub _eval_keyword_additionalItems ($data, $schema, $state) {
  219         336  
  219         280  
  219         264  
  219         233  
819 219 100       461 return 1 if not exists $state->{_last_items_index};
820 191         665 goto \&_eval_keyword__items_schema;
821             }
822              
823             # prefixItems (draft 2020-12), array-based items (all drafts)
824 1110     1110   1322 sub _eval_keyword__items_array_schemas ($data, $schema, $state) {
  1110         1348  
  1110         1158  
  1110         1298  
  1110         1070  
825 1110 50       2217 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
826 1110 100       1755 return 1 if not is_type('array', $data);
827              
828 897         1235 my $valid = 1;
829              
830 897         2188 foreach my $idx (0..$data->$#*) {
831 1581 100       4493 last if $idx > $schema->{$state->{keyword}}->$#*;
832 1294         2241 $state->{_last_items_index} = $idx;
833              
834 1294 100       2302 if (is_type('boolean', $schema->{$state->{keyword}}[$idx])) {
835 286 100       908 next if $schema->{$state->{keyword}}[$idx];
836 108         1456 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx,
837             _schema_path_suffix => $idx }, 'item not permitted');
838             }
839             else {
840             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}}[$idx],
841             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
842 1008 100       9775 schema_path => $state->{schema_path}.'/'.$state->{keyword}.'/'.$idx });
843             }
844              
845 175         688 $valid = 0;
846             last if $state->{short_circuit} and not exists $schema->{
847             $state->{keyword} eq 'prefixItems' ? 'items'
848 175 50 100     817 : $state->{keyword} eq 'items' ? 'additionalItems' : die
    100          
    100          
849             };
850             }
851              
852 897 100       1848 return E($state, 'not all items are valid') if not $valid;
853 725         1366 return 1;
854             }
855              
856             # schema-based items (all drafts), and additionalItems (drafts 4,6,7,2019-09)
857 795     795   1049 sub _eval_keyword__items_schema ($data, $schema, $state) {
  795         917  
  795         889  
  795         965  
  795         908  
858 795 100       1399 return 1 if not is_type('array', $data);
859 691 100       1734 return 1 if $state->{_last_items_index} == $data->$#*;
860              
861 447         611 my $valid = 1;
862 447         1148 foreach my $idx ($state->{_last_items_index}+1 .. $data->$#*) {
863 676 100 100     1548 if (is_type('boolean', $schema->{$state->{keyword}})
864             and ($state->{keyword} eq 'additionalItems')) {
865 32 100       174 next if $schema->{$state->{keyword}};
866             $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
867             '%sitem not permitted',
868 26 50 33     506 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '');
869             }
870             else {
871             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}},
872             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
873 644 100       6295 schema_path => $state->{schema_path}.'/'.$state->{keyword} });
874 219         741 $valid = 0;
875             }
876              
877 245 100       744 last if $state->{short_circuit};
878             }
879              
880 382         913 $state->{_last_items_index} = $data->$#*;
881              
882             return E($state, 'subschema is not valid against all %sitems',
883 382 100 100     1312 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '')
    100          
884             if not $valid;
885 179         353 return 1;
886             }
887              
888 717     717   880 sub _eval_keyword_contains ($data, $schema, $state) {
  717         949  
  717         861  
  717         799  
  717         774  
889 717 100       1294 return 1 if not is_type('array', $data);
890              
891 504         986 $state->{_num_contains} = 0;
892 504         649 my @errors;
893 504         1406 foreach my $idx (0..$data->$#*) {
894 622 100       7642 if (_eval_subschema($data->[$idx], $schema->{contains},
895             +{ %$state, errors => \@errors,
896             data_path => $state->{data_path}.'/'.$idx,
897             schema_path => $state->{schema_path}.'/contains' })) {
898 390         1933 ++$state->{_num_contains};
899              
900             last if $state->{short_circuit}
901             and (not exists $schema->{maxContains} or $state->{_num_contains} > $schema->{maxContains})
902 390 100 100     2387 and ($state->{_num_contains} >= ($schema->{minContains}//1));
      100        
      100        
      100        
903             }
904             }
905              
906             # note: no items contained is only valid when minContains is explicitly 0
907 504 100 66     4492 if (not $state->{_num_contains} and (($schema->{minContains}//1) > 0
      66        
908             or $state->{spec_version} and $state->{spec_version} eq 'draft7')) {
909 195         387 push $state->{errors}->@*, @errors;
910 195         368 return E($state, 'subschema is not valid against any item');
911             }
912              
913 309         826 return 1;
914             }
915              
916 2401     2401   2959 sub _eval_keyword_properties ($data, $schema, $state) {
  2401         3035  
  2401         3017  
  2401         2610  
  2401         2500  
917 2401         5035 assert_keyword_type($state, $schema, 'object');
918 2401 100       3736 return 1 if not is_type('object', $data);
919              
920 2150         2760 my $valid = 1;
921 2150         6117 foreach my $property (sort keys $schema->{properties}->%*) {
922 2714 100       4908 next if not exists $data->{$property};
923              
924 1670 100       2937 if (is_type('boolean', $schema->{properties}{$property})) {
925 323 100       1211 next if $schema->{properties}{$property};
926 106         950 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
927             _schema_path_suffix => $property }, 'property not permitted');
928             }
929             else {
930             next if _eval_subschema($data->{$property}, $schema->{properties}{$property},
931             +{ %$state,
932             data_path => jsonp($state->{data_path}, $property),
933 1347 100       5411 schema_path => jsonp($state->{schema_path}, 'properties', $property) });
934              
935 329         1100 $valid = 0;
936             }
937 435 100       1446 last if $state->{short_circuit};
938             }
939              
940 1995 100       5052 return E($state, 'not all properties are valid') if not $valid;
941 1582         3023 return 1;
942             }
943              
944 809     809   1036 sub _eval_keyword_patternProperties ($data, $schema, $state) {
  809         1065  
  809         1011  
  809         939  
  809         833  
945 809         1743 assert_keyword_type($state, $schema, 'object');
946              
947 809         2542 foreach my $property (sort keys $schema->{patternProperties}->%*) {
948 1250         7336 assert_pattern({ %$state, _schema_path_suffix => $property }, $property);
949             }
950              
951 807 100       1454 return 1 if not is_type('object', $data);
952              
953 614         814 my $valid = 1;
954 614         1387 foreach my $property_pattern (sort keys $schema->{patternProperties}->%*) {
955 898         9198 foreach my $property (sort grep m/(?:$property_pattern)/, keys %$data) {
956 557 100       1262 if (is_type('boolean', $schema->{patternProperties}{$property_pattern})) {
957 319 100       1184 next if $schema->{patternProperties}{$property_pattern};
958 108         924 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
959             _schema_path_suffix => $property_pattern }, 'property not permitted');
960             }
961             else {
962             next if _eval_subschema($data->{$property}, $schema->{patternProperties}{$property_pattern},
963             +{ %$state,
964             data_path => jsonp($state->{data_path}, $property),
965 238 100       861 schema_path => jsonp($state->{schema_path}, 'patternProperties', $property_pattern) });
966              
967 87         282 $valid = 0;
968             }
969 195 100       835 last if $state->{short_circuit};
970             }
971             }
972              
973 614 100       2573 return E($state, 'not all properties are valid') if not $valid;
974 434         900 return 1;
975             }
976              
977 755     755   992 sub _eval_keyword_additionalProperties ($data, $schema, $state) {
  755         946  
  755         897  
  755         845  
  755         869  
978 755 100       1284 return 1 if not is_type('object', $data);
979              
980 556         813 my $valid = 1;
981 556         1364 foreach my $property (sort keys %$data) {
982 552 100 100     1598 next if exists $schema->{properties} and exists $schema->{properties}{$property};
983             next if exists $schema->{patternProperties}
984 438 100 100     1382 and any { $property =~ /(?:$_)/ } keys $schema->{patternProperties}->%*;
  148         1584  
985              
986 350 100       660 if (is_type('boolean', $schema->{additionalProperties})) {
987 192 100       735 next if $schema->{additionalProperties};
988              
989 172         1634 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
990             'additional property not permitted');
991             }
992             else {
993             next if _eval_subschema($data->{$property}, $schema->{additionalProperties},
994             +{ %$state,
995             data_path => jsonp($state->{data_path}, $property),
996 158 100       741 schema_path => $state->{schema_path}.'/additionalProperties' });
997              
998 43         156 $valid = 0;
999             }
1000 215 100       1076 last if $state->{short_circuit};
1001             }
1002              
1003 504 100       1217 return E($state, 'not all additional properties are valid') if not $valid;
1004 290         622 return 1;
1005             }
1006              
1007 463     463   542 sub _eval_keyword_propertyNames ($data, $schema, $state) {
  463         599  
  463         513  
  463         495  
  463         482  
1008 463 100       839 return 1 if not is_type('object', $data);
1009              
1010 288         359 my $valid = 1;
1011 288         657 foreach my $property (sort keys %$data) {
1012             next if _eval_subschema($property, $schema->{propertyNames},
1013             +{ %$state,
1014             data_path => jsonp($state->{data_path}, $property),
1015 202 100       738 schema_path => $state->{schema_path}.'/propertyNames' });
1016              
1017 116         398 $valid = 0;
1018 116 100       304 last if $state->{short_circuit};
1019             }
1020              
1021 288 100       661 return E($state, 'not all property names are valid') if not $valid;
1022 172         302 return 1;
1023             }
1024              
1025 384     384   436 sub _eval_keyword_unevaluatedItems ($data, $schema, $state) {
  384         429  
  384         415  
  384         440  
  384         379  
1026 384         678 abort($state, 'keyword not yet supported');
1027             }
1028              
1029 584     584   712 sub _eval_keyword_unevaluatedProperties ($data, $schema, $state) {
  584         746  
  584         720  
  584         626  
  584         617  
1030 584         1088 abort($state, 'keyword not yet supported');
1031             }
1032              
1033             # UTILITIES
1034              
1035             # supports the six core types, plus integer (which is also a number)
1036             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1037             # note that sometimes a value may return true for more than one type, e.g. integer+number,
1038             # or number+string, depending on its internal flags.
1039             # copied from JSON::Schema::Modern::Utilities::is_type
1040 52894     52894 0 903184 sub is_type ($type, $value) {
  52894         57373  
  52894         58913  
  52894         50553  
1041 52894 100       78292 if ($type eq 'null') {
1042 83         319 return !(defined $value);
1043             }
1044 52811 100       72839 if ($type eq 'boolean') {
1045 6022         9562 return is_bool($value);
1046             }
1047 46789 100       69968 if ($type eq 'object') {
1048 12088         30939 return ref $value eq 'HASH';
1049             }
1050 34701 100       50288 if ($type eq 'array') {
1051 8790         23654 return ref $value eq 'ARRAY';
1052             }
1053              
1054 25911 100 100     56031 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
1055 25895 100       36569 return 0 if not defined $value;
1056 25877         85158 my $flags = B::svref_2object(\$value)->FLAGS;
1057              
1058             # dualvars with the same string and (stringified) numeric value could be either a string or a
1059             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
1060             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
1061             # numified strings do have POK set, so we can tell which one came first.
1062              
1063 25877 100       48199 if ($type eq 'string') {
1064             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
1065             return !length ref($value)
1066             && $flags & B::SVf_POK
1067             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
1068 17   100 17   161243 || do { no warnings 'numeric'; 0+$value eq $value });
  17         35  
  17         7320  
  16845         92301  
1069             }
1070              
1071 9032 100       13634 if ($type eq 'number') {
1072             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1073 6143   100     9081 return is_bignum($value) || created_as_number($value);
1074             }
1075              
1076 2889 50       4204 if ($type eq 'integer') {
1077             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
1078             # therefore they will fail this check
1079 2889   100     4669 return is_bignum($value) && $value->is_int
1080             # if dualvar, PV and stringified NV/IV must be identical
1081             || created_as_number($value) && int($value) == $value;
1082             }
1083             }
1084              
1085 16 100       68 if ($type =~ /^reference to (.+)$/) {
1086 11   33     78 return !blessed($value) && ref($value) eq $1;
1087             }
1088              
1089 5         22 return ref($value) eq $type;
1090             }
1091              
1092             # returns one of the six core types, plus integer
1093             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1094             # copied from JSON::Schema::Modern::Utilities::get_type
1095 35088     35088 0 732683 sub get_type ($value) {
  35088         42981  
  35088         38135  
1096 35088 100       77437 return 'object' if ref $value eq 'HASH';
1097 13041 100       18819 return 'boolean' if is_bool($value);
1098 11062 100       23745 return 'null' if not defined $value;
1099 10764 100       16576 return 'array' if ref $value eq 'ARRAY';
1100              
1101             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1102 9481 100       15424 if (length(my $ref = ref $value)) {
1103 455 100       1777 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         23140 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   122 || do { no warnings 'numeric'; 0+$value eq $value });
  17   100     216  
  17         2432  
  9026         26020  
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       16122 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         59 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   139 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  17         27  
  17         1433  
1135 17     17   87 use if HAVE_BUILTIN, experimental => 'builtin';
  17         28  
  17         499  
1136 19063     19063 0 20284 sub is_bool ($value) {
  19063         20684  
  19063         17907  
1137 19063 50 66     74381 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 12860 sub is_bignum ($value) {
  11375         12143  
  11375         10841  
1146 11375         52990 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 5127 sub is_equal ($x, $y, $state = {}) {
  4171         4819  
  4171         4970  
  4171         5041  
  4171         4494  
1158 4171   100     13668 $state->{path} //= '';
1159              
1160 4171         8055 my @types = map get_type($_), $x, $y;
1161              
1162 4171 100       11260 $state->{error} = 'ambiguous type encountered', return 0
1163             if grep $types[$_] eq 'ambiguous type', 0..1;
1164              
1165 4168 100       7465 if ($SCALARREF_BOOLEANS) {
1166 99 100       149 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
1167 99 100       150 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
1168             }
1169              
1170 4168 100       6244 if ($STRINGY_NUMBERS) {
1171 18 100 100     114 ($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     125 ($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       9004 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
1179 3325 100       5313 return 1 if $types[0] eq 'null';
1180 3311 100 100     10073 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
1181             if $types[0] eq 'string';
1182 1778 100 100     7000 ($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         901 my $path = $state->{path};
1186 623 100       1062 if ($types[0] eq 'object') {
1187 217 100       538 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
1188             if keys %$x != keys %$y;
1189              
1190 200 100       972 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
1191 7         18 my $pos = substr($s->{path}, 1);
1192 7         21 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
1193 7         34 return 0;
1194             }
1195              
1196 193         483 foreach my $property (sort keys %$x) {
1197 231         398 $state->{path} = jsonp($path, $property);
1198 231 100       434 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
1199             }
1200              
1201 106         664 return 1;
1202             }
1203              
1204 406 50       730 if ($types[0] eq 'array') {
1205 406 100       756 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
1206 397         834 foreach my $idx (0 .. $x->$#*) {
1207 441         980 $state->{path} = $path.'/'.$idx;
1208 441 100       1090 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
1209             }
1210 269         4860 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 2631 sub is_elements_unique ($array, $equal_indices = undef) {
  2358         2637  
  2358         3056  
  2358         2504  
1223 2358         5571 foreach my $idx0 (0 .. $array->$#*-1) {
1224 846         1799 foreach my $idx1 ($idx0+1 .. $array->$#*) {
1225 1251 100       2551 if (is_equal($array->[$idx0], $array->[$idx1])) {
1226 207 50       2013 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
1227 207         509 return 0;
1228             }
1229             }
1230             }
1231 2151         5022 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 145880 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 34710 sub canonical_uri ($state, @extra_path) {
  30612         32337  
  30612         35460  
  30612         33155  
1245 30612 100 100     88595 return $state->{initial_schema_uri} if not @extra_path and not length($state->{schema_path});
1246 16601         42433 my $uri = $state->{initial_schema_uri}->clone;
1247 16601 100 100     1038475 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{schema_path}, @extra_path) : $state->{schema_path});
1248 16601 100       58775 undef $fragment if not length($fragment);
1249 16601         33680 $uri->fragment($fragment);
1250 16601         79154 $uri;
1251             }
1252              
1253             # shorthand for creating error objects
1254             # based on JSON::Schema::Modern::Utilities::E
1255 9843     9843 0 28487 sub E ($state, $error_string, @args) {
  9843         10887  
  9843         11278  
  9843         13513  
  9843         9749  
1256             # sometimes the keyword shouldn't be at the very end of the schema path
1257 9843         15500 my $sps = delete $state->{_schema_path_suffix};
1258 9843 100 100     35273 my @schema_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
1259              
1260 9843         18378 my $uri = canonical_uri($state, $state->{keyword}, @schema_path_suffix);
1261              
1262             my $keyword_location = $state->{traversed_schema_path}
1263 9843         20865 .jsonp($state->{schema_path}, $state->{keyword}, @schema_path_suffix);
1264              
1265 9843 100 100     23373 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       2569918 keywordLocation => $keyword_location,
    100          
1271             defined $uri ? ( absoluteKeywordLocation => $uri->to_string) : (),
1272             error => @args ? sprintf($error_string, @args) : $error_string,
1273             };
1274              
1275 9843         207375 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 223613 sub abort ($state, $error_string, @args) {
  1674         1977  
  1674         2017  
  1674         1970  
  1674         1800  
1282 1674         3693 E($state, $error_string, @args);
1283 1674         25806 die pop $state->{errors}->@*;
1284             }
1285              
1286             # one common usecase of abort()
1287 28018     28018 0 31396 sub assert_keyword_type ($state, $schema, $type) {
  28018         30113  
  28018         30109  
  28018         31105  
  28018         28768  
1288 28018 100       59494 return 1 if is_type($type, $schema->{$state->{keyword}});
1289 18 100       88 abort($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
1290             }
1291              
1292 2149     2149 0 2379 sub assert_pattern ($state, $pattern) {
  2149         2357  
  2149         2380  
  2149         2402  
1293 2149         3070 try {
1294 2149     1   11898 local $SIG{__WARN__} = sub { die @_ };
  1         11  
1295 2149         23310 qr/$pattern/;
1296             }
1297 3         66 catch ($e) { abort($state, $e); }
1298 2146         6149 return 1;
1299             }
1300              
1301             # based on JSON::Schema::Modern::Utilities::assert_uri_reference
1302 2421     2421 0 2559 sub assert_uri_reference ($state, $schema) {
  2421         2696  
  2421         2699  
  2421         2512  
1303 2421         3886 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     6183 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         659816 return 1;
1314             }
1315              
1316             # based on JSON::Schema::Modern::Utilities::assert_uri
1317 5648     5648 0 6050 sub assert_uri ($state, $schema, $override = undef) {
  5648         6100  
  5648         5708  
  5648         7062  
  5648         5738  
1318 5648   66     15268 my $string = $override // $schema->{$state->{keyword}};
1319 5648         14741 my $uri = Mojo::URL->new($string);
1320              
1321 5648 0 33     385304 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         934221 return 1;
1332             }
1333              
1334 2788     2788 0 3130 sub assert_non_negative_integer ($schema, $state) {
  2788         3076  
  2788         2959  
  2788         2852  
1335 2788         5160 assert_keyword_type($state, $schema, 'integer');
1336             abort($state, '%s value is not a non-negative integer', $state->{keyword})
1337 2788 50       7268 if $schema->{$state->{keyword}} < 0;
1338 2788         32506 return 1;
1339             }
1340              
1341 1928     1928 0 2334 sub assert_array_schemas ($schema, $state) {
  1928         2379  
  1928         2139  
  1928         2273  
1342 1928         3942 assert_keyword_type($state, $schema, 'array');
1343 1928 50       4370 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
1344 1928         2739 return 1;
1345             }
1346              
1347             # copied from JSON::Schema::Modern::Utilities::sprintf_num
1348 989     989 0 1253 sub sprintf_num ($value) {
  989         1237  
  989         1131  
1349             # use original value as stored in the NV, without losing precision
1350 989 100       1501 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
1351             }
1352              
1353             1;
1354              
1355             __END__