File Coverage

blib/lib/JSON/Schema/Tiny.pm
Criterion Covered Total %
statement 913 919 99.3
branch 512 564 90.7
condition 344 425 80.9
subroutine 100 101 99.0
pod 1 19 5.2
total 1870 2028 92.2


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   5019367 use strictures 2;
  17         140  
  17         673  
3             package JSON::Schema::Tiny; # git description: v0.031-7-g76bbeca
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.032';
9              
10 17     17   7546 use 5.020; # for unicode_strings, signatures, postderef features
  17         56  
11 17     17   86 use stable 0.031 'postderef';
  17         247  
  17         103  
12 17     17   3067 use experimental 0.026 qw(signatures args_array_with_signatures);
  17         254  
  17         76  
13 17     17   1223 no autovivification warn => qw(fetch store exists delete);
  17         27  
  17         101  
14 17     17   1111 use if "$]" >= 5.022, experimental => 're_strict';
  17         24  
  17         424  
15 17     17   1064 no if "$]" >= 5.031009, feature => 'indirect';
  17         24  
  17         964  
16 17     17   159 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         73  
  17         863  
17 17     17   70 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         25  
  17         763  
18 17     17   93 use B;
  17         22  
  17         400  
19 17     17   8247 use Mojo::URL;
  17         1962470  
  17         122  
20 17     17   7820 use Mojo::JSON::Pointer;
  17         12730  
  17         98  
21 17     17   662 use Carp qw(croak carp);
  17         26  
  17         983  
22 17     17   4489 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  17         261937  
  17         498  
23 17     17   4828 use Feature::Compat::Try;
  17         3443  
  17         142  
24 17     17   1154 use JSON::PP ();
  17         24  
  17         600  
25 17     17   67 use if "$]" < 5.041010, 'List::Util' => 'any';
  17         72  
  17         737  
26 17     17   79 use if "$]" >= 5.041010, experimental => 'keyword_any';
  17         21  
  17         297  
27 17     17   980 use Scalar::Util 'looks_like_number';
  17         26  
  17         869  
28 17     17   12858 use builtin::compat qw(blessed created_as_number);
  17         185619  
  17         85  
29 17     17   2931 use if "$]" >= 5.022, POSIX => 'isinf';
  17         24  
  17         7667  
30 17     17   138168 use Math::BigFloat;
  17         1296501  
  17         89  
31 17     17   423938 use namespace::clean;
  17         31  
  17         185  
32 17     17   4688 use Exporter 5.57 'import';
  17         273  
  17         18958  
33              
34             our @EXPORT_OK = qw(evaluate);
35              
36             our $BOOLEAN_RESULT = 0;
37             our $SHORT_CIRCUIT = 0;
38             our $MAX_TRAVERSAL_DEPTH = 50;
39             our $MOJO_BOOLEANS; # deprecated; renamed to $SCALARREF_BOOLEANS
40             our $SCALARREF_BOOLEANS;
41             our $STRINGY_NUMBERS;
42             our $SPECIFICATION_VERSION;
43              
44             my %version_uris = (
45             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12',
46             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09',
47             'http://json-schema.org/draft-07/schema#' => 'draft7',
48             );
49              
50 18     18 0 2904048 sub new ($class, %args) {
  18         34  
  18         41  
  18         27  
51 18         63 bless(\%args, $class);
52             }
53              
54             sub evaluate {
55 12777 50   12777 1 22407842 croak 'evaluate called in void context' if not defined wantarray;
56              
57 12777   66     42403 $SCALARREF_BOOLEANS = $SCALARREF_BOOLEANS // $MOJO_BOOLEANS;
58             local $BOOLEAN_RESULT = $_[0]->{boolean_result} // $BOOLEAN_RESULT,
59             local $SHORT_CIRCUIT = $_[0]->{short_circuit} // $SHORT_CIRCUIT,
60             local $MAX_TRAVERSAL_DEPTH = $_[0]->{max_traversal_depth} // $MAX_TRAVERSAL_DEPTH,
61             local $SCALARREF_BOOLEANS = $_[0]->{scalarref_booleans} // $SCALARREF_BOOLEANS // $_[0]->{mojo_booleans},
62             local $STRINGY_NUMBERS = $_[0]->{stringy_numbers} // $STRINGY_NUMBERS,
63 12777 100 33     219769 local $SPECIFICATION_VERSION = $_[0]->{specification_version} // $SPECIFICATION_VERSION,
      66        
      66        
      33        
      33        
      33        
      66        
      100        
64             shift
65             if blessed($_[0]) and blessed($_[0])->isa(__PACKAGE__);
66              
67 12777 100       28195 if (defined $SPECIFICATION_VERSION) {
68             $SPECIFICATION_VERSION = 'draft'.$SPECIFICATION_VERSION
69 12631 100 100     36282 if $SPECIFICATION_VERSION !~ /^draft/ and any { 'draft'.$SPECIFICATION_VERSION eq $_ } values %version_uris;
  9         44  
70              
71 12631 100       31945 croak '$SPECIFICATION_VERSION value is invalid' if not any { $SPECIFICATION_VERSION eq $_ } values %version_uris;
  22382         56856  
72             }
73              
74 12776 50       24000 croak 'insufficient arguments' if @_ < 2;
75 12776         22147 my ($data, $schema) = @_;
76              
77 12776   100     42531 my $state = {
78             depth => 0,
79             data_path => '',
80             traversed_schema_path => '', # the accumulated traversal path up to the last $ref traversal
81             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start or the last traversed $ref
82             schema_path => '', # the rest of the path, since the start or the last traversed $ref
83             errors => [],
84             seen => {},
85             short_circuit => $BOOLEAN_RESULT || $SHORT_CIRCUIT,
86             root_schema => $schema, # so we can do $refs within the same document
87             spec_version => $SPECIFICATION_VERSION,
88             };
89              
90 12776         179945 my $valid;
91 12776         20204 try {
92 12776         28653 $valid = _eval_subschema($data, $schema, $state)
93             }
94             catch ($e) {
95 1675 100       4564 if (ref $e eq 'HASH') {
96 1674         3818 push $state->{errors}->@*, $e;
97             }
98             else {
99 1         5 E($state, 'EXCEPTION: '.$e);
100             }
101              
102 1675         3732 $valid = 0;
103             }
104              
105 12776 50 66     38214 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
106              
107             return $BOOLEAN_RESULT ? $valid : +{
108             valid => $valid ? JSON::PP::true : JSON::PP::false,
109 12776 100       53395 $valid ? () : (errors => $state->{errors}),
    100          
    100          
110             };
111             }
112              
113             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
114              
115             # current spec version => { keyword => undef, or arrayref of alternatives }
116             my %removed_keywords = (
117             'draft7' => {
118             id => [ '$id' ],
119             },
120             'draft2019-09' => {
121             id => [ '$id' ],
122             definitions => [ '$defs' ],
123             dependencies => [ qw(dependentSchemas dependentRequired) ],
124             },
125             'draft2020-12' => {
126             id => [ '$id' ],
127             definitions => [ '$defs' ],
128             dependencies => [ qw(dependentSchemas dependentRequired) ],
129             '$recursiveAnchor' => [ '$dynamicAnchor' ],
130             '$recursiveRef' => [ '$dynamicRef' ],
131             additionalItems => [ 'items' ],
132             },
133             );
134              
135 21636     21636   27110 sub _eval_subschema ($data, $schema, $state) {
  21636         26967  
  21636         24036  
  21636         23237  
  21636         22658  
136 21636 50       35145 croak '_eval_subschema called in void context' if not defined wantarray;
137              
138             # do not propagate upwards changes to depth, traversed paths,
139             # but additions to errors are by reference and will be retained
140 21636         93638 $state = { %$state };
141 21636         133007 delete $state->@{'keyword', grep /^_/, keys %$state};
142              
143             abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
144 21636 100       62268 if $state->{depth}++ > $MAX_TRAVERSAL_DEPTH;
145              
146 21633         41289 my $schema_type = get_type($schema);
147 21633 100 66     38758 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
148 20882 100       35426 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
149              
150 20863 100       42776 return 1 if not keys %$schema;
151              
152             # find all schema locations in effect at this data path + canonical_uri combination
153             # if any of them are absolute prefix of this schema location, we are in a loop.
154 20504         35199 my $canonical_uri = canonical_uri($state);
155 20504         45449 my $schema_location = $state->{traversed_schema_path}.$state->{schema_path};
156             {
157 17     17   128 use autovivification qw(fetch store);
  17         30  
  17         132  
  20504         23621  
158             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
159             if grep substr($schema_location, 0, length) eq $_,
160 20504 100       74665 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
161 20502         2914637 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
162             }
163              
164 20502         1953244 my $valid = 1;
165 20502   100     57130 my $spec_version = $state->{spec_version}//'';
166              
167 20502 100 100     568437 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        
168             # CORE KEYWORDS
169             qw($id $schema),
170             !$spec_version || $spec_version ne 'draft7' ? '$anchor' : (),
171             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveAnchor' : (),
172             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicAnchor' : (),
173             '$ref',
174             !$spec_version || $spec_version eq 'draft2019-09' ? '$recursiveRef' : (),
175             !$spec_version || $spec_version eq 'draft2020-12' ? '$dynamicRef' : (),
176             !$spec_version || $spec_version ne 'draft7' ? '$vocabulary' : (),
177             '$comment',
178             !$spec_version || $spec_version eq 'draft7' ? 'definitions' : (),
179             !$spec_version || $spec_version ne 'draft7' ? '$defs' : (),
180             # APPLICATOR KEYWORDS
181             qw(allOf anyOf oneOf not if),
182             !$spec_version || $spec_version ne 'draft7' ? 'dependentSchemas' : (),
183             !$spec_version || $spec_version eq 'draft7' ? 'dependencies' : (),
184             !$spec_version || $spec_version !~ qr/^draft(?:7|2019-09)$/ ? 'prefixItems' : (),
185             'items',
186             !$spec_version || $spec_version =~ qr/^draft(?:7|2019-09)$/ ? 'additionalItems' : (),
187             qw(contains properties patternProperties additionalProperties propertyNames),
188             # UNEVALUATED KEYWORDS
189             !$spec_version || $spec_version ne 'draft7' ? qw(unevaluatedItems unevaluatedProperties) : (),
190             # VALIDATOR KEYWORDS
191             qw(type enum const
192             multipleOf maximum exclusiveMaximum minimum exclusiveMinimum
193             maxLength minLength pattern
194             maxItems minItems uniqueItems),
195             !$spec_version || $spec_version ne 'draft7' ? qw(maxContains minContains) : (),
196             qw(maxProperties minProperties required),
197             !$spec_version || $spec_version ne 'draft7' ? 'dependentRequired' : (),
198             ) {
199 720298 100       1078180 next if not exists $schema->{$keyword};
200              
201             # keywords adjacent to $ref (except for definitions) are not evaluated before draft2019-09
202             next if $keyword ne '$ref' and $keyword ne 'definitions'
203 34296 100 100     139401 and exists $schema->{'$ref'} and $spec_version eq 'draft7';
      100        
      100        
204              
205 34281         66061 $state->{keyword} = $keyword;
206 34281         55647 my $error_count = $state->{errors}->@*;
207              
208 34281         194905 my $sub = __PACKAGE__->can('_eval_keyword_'.($keyword =~ s/^\$//r));
209 34281 100       73618 if (not $sub->($data, $schema, $state)) {
210             warn 'result is false but there are no errors (keyword: '.$keyword.')'
211 7501 50       18652 if $error_count == $state->{errors}->@*;
212 7501         10256 $valid = 0;
213             }
214              
215 31682 100 100     177614 last if not $valid and $state->{short_circuit};
216             }
217              
218             # check for previously-supported but now removed keywords
219 17903   100     109457 foreach my $keyword (sort keys(($removed_keywords{$spec_version}//{})->%*)) {
220 62636 100       103753 next if not exists $schema->{$keyword};
221 214         441 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
222             .canonical_uri($state).'")';
223 214 50       18866 if (my $alternates = $removed_keywords{$spec_version}->{$keyword}) {
224 214         797 my @list = map '"'.$_.'"', @$alternates;
225 214 50       482 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
226 214 100       520 splice(@list, -1, 0, 'or') if @list > 1;
227 214         533 $message .= ': this should be rewritten as '.join(' ', @list);
228             }
229 214         23908 carp $message;
230             }
231              
232 17903         110685 return $valid;
233             }
234              
235             # KEYWORD IMPLEMENTATIONS
236              
237 5644     5644   6994 sub _eval_keyword_schema ($data, $schema, $state) {
  5644         8506  
  5644         6135  
  5644         6188  
  5644         5774  
238 5644         12752 assert_keyword_type($state, $schema, 'string');
239 5644         14018 assert_uri($state, $schema);
240              
241             return abort($state, '$schema can only appear at the schema resource root')
242 5644 100       15391 if length($state->{schema_path});
243              
244 5643         14089 my $spec_version = $version_uris{$schema->{'$schema'}};
245 5643 100       10134 abort($state, 'custom $schema URIs are not supported (must be one of: %s',
246             join(', ', map '"'.$_.'"', keys %version_uris))
247             if not $spec_version;
248              
249 5614 100 100     15984 abort($state, '"$schema" indicates a different version than that requested by $JSON::Schema::Tiny::SPECIFICATION_VERSION')
250             if defined $SPECIFICATION_VERSION and $SPECIFICATION_VERSION ne $spec_version;
251              
252             # we special-case this because the check in _eval for older drafts + $ref has already happened
253             abort($state, '$schema and $ref cannot be used together in older drafts')
254 5613 100 100     15719 if exists $schema->{'$ref'} and $spec_version eq 'draft7';
255              
256 5612         17230 $state->{spec_version} = $spec_version;
257             }
258              
259 1699     1699   2060 sub _eval_keyword_ref ($data, $schema, $state) {
  1699         2323  
  1699         1999  
  1699         1839  
  1699         1850  
260 1699         3813 assert_keyword_type($state, $schema, 'string');
261 1699         4197 assert_uri_reference($state, $schema);
262              
263 1699         7209 my $uri = Mojo::URL->new($schema->{$state->{keyword}})->to_abs($state->{initial_schema_uri});
264             abort($state, '%ss to anchors are not supported', $state->{keyword})
265 1699 100 100     546063 if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
266              
267             # the base of the $ref uri must be the same as the base of the root schema
268             # unfortunately this means that many uses of $ref won't work, because we don't
269             # track the locations of $ids in this or other documents.
270             abort($state, 'only same-document, same-base JSON pointers are supported in %s', $state->{keyword})
271 1601 100 100     16988 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
272              
273 1132   100     456210 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment//'');
274 1132 100       39319 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
275              
276             return _eval_subschema($data, $subschema,
277             +{ %$state,
278             traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/'.$state->{keyword},
279 1127         11390 initial_schema_uri => $uri,
280             schema_path => '',
281             });
282             }
283              
284 52     52   61 sub _eval_keyword_recursiveRef ($data, $schema, $state) {
  52         77  
  52         60  
  52         51  
  52         69  
285 52         127 assert_keyword_type($state, $schema, 'string');
286 52         126 assert_uri_reference($state, $schema);
287              
288 52         191 my $uri = Mojo::URL->new($schema->{'$recursiveRef'})->to_abs($state->{initial_schema_uri});
289 52 50 50     17150 abort($state, '$recursiveRefs to anchors are not supported')
290             if ($uri->fragment//'') !~ m{^(?:/(?:[^~]|~[01])*)?$};
291              
292             # the base of the $recursiveRef uri must be the same as the base of the root schema.
293             # unfortunately this means that nearly all usecases of $recursiveRef won't work, because we don't
294             # track the locations of $ids in this or other documents.
295             abort($state, 'only same-document, same-base JSON pointers are supported in $recursiveRef')
296 52 100 100     435 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
297              
298 8         2876 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
299 8 50       143 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
300              
301 8 50 33     26 if (is_type('boolean', $subschema->{'$recursiveAnchor'}) and $subschema->{'$recursiveAnchor'}) {
302             $uri = Mojo::URL->new($schema->{'$recursiveRef'})
303 0   0     0 ->to_abs($state->{recursive_anchor_uri} // $state->{initial_schema_uri});
304 0         0 $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
305 0 0       0 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
306             }
307              
308             return _eval_subschema($data, $subschema,
309             +{ %$state,
310 8         74 traversed_schema_path => $state->{traversed_schema_path}.$state->{schema_path}.'/$recursiveRef',
311             initial_schema_uri => $uri,
312             schema_path => '',
313             });
314             }
315              
316 12     12   32 sub _eval_keyword_dynamicRef { goto \&_eval_keyword_ref }
317              
318 670     670   974 sub _eval_keyword_id ($data, $schema, $state) {
  670         960  
  670         887  
  670         850  
  670         789  
319 670         1758 assert_keyword_type($state, $schema, 'string');
320 670         1818 assert_uri_reference($state, $schema);
321              
322 670         2475 my $uri = Mojo::URL->new($schema->{'$id'});
323              
324 670 100 100     59547 if (($state->{spec_version}//'') eq 'draft7') {
325 133 100       305 if (length($uri->fragment)) {
326 3 50       17 abort($state, '$id cannot change the base uri at the same time as declaring an anchor')
327             if length($uri->clone->fragment(undef));
328              
329 3 100       386 abort($state, '$id value does not match required syntax')
330             if $uri->fragment !~ m/^[A-Za-z][A-Za-z0-9_:.-]*$/;
331              
332 2         21 return 1;
333             }
334             }
335             else {
336 537 100       1163 abort($state, '$id value "%s" cannot have a non-empty fragment', $uri) if length $uri->fragment;
337             }
338              
339 665         3155 $uri->fragment(undef);
340 665 100       3735 return E($state, '$id cannot be empty') if not length $uri;
341              
342 641 100       87711 $state->{initial_schema_uri} = $uri->is_abs ? $uri : $uri->to_abs($state->{initial_schema_uri});
343 641         42056 $state->{traversed_schema_path} = $state->{traversed_schema_path}.$state->{schema_path};
344 641         1322 $state->{schema_path} = '';
345              
346 641         1876 return 1;
347             }
348              
349 12     12   21 sub _eval_keyword_anchor ($data, $schema, $state) {
  12         18  
  12         15  
  12         26  
  12         13  
350 12         34 assert_keyword_type($state, $schema, 'string');
351              
352             return 1 if
353             (!$state->{spec_version} or $state->{spec_version} eq 'draft2019-09')
354             and ($schema->{'$anchor'}//'') =~ /^[A-Za-z][A-Za-z0-9_:.-]*$/
355             or
356             (!$state->{spec_version} or $state->{spec_version} eq 'draft2020-12')
357 12 50 66     153 and ($schema->{'$anchor'}//'') =~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
      50        
      66        
      33        
      50        
      33        
      66        
358              
359 0         0 abort($state, '$anchor value does not match required syntax');
360             }
361              
362 92     92   132 sub _eval_keyword_recursiveAnchor ($data, $schema, $state) {
  92         129  
  92         111  
  92         92  
  92         95  
363 92         192 assert_keyword_type($state, $schema, 'boolean');
364 92 100 100     332 return 1 if not $schema->{'$recursiveAnchor'} or exists $state->{recursive_anchor_uri};
365              
366             # this is required because the location is used as the base URI for future resolution
367             # of $recursiveRef, and the fragment would be disregarded in the base
368             abort($state, '"$recursiveAnchor" keyword used without "$id"')
369 51 50       394 if not exists $schema->{'$id'};
370              
371             # record the canonical location of the current position, to be used against future resolution
372             # of a $recursiveRef uri -- as if it was the current location when we encounter a $ref.
373 51         91 $state->{recursive_anchor_uri} = canonical_uri($state);
374              
375 51         128 return 1;
376             }
377              
378 10     10   14 sub _eval_keyword_dynamicAnchor ($data, $schema, $state) {
  10         12  
  10         10  
  10         12  
  10         9  
379 10 50       19 return if not assert_keyword_type($state, $schema, 'string');
380              
381             abort($state, '$dynamicAnchor value does not match required syntax')
382 10 50       58 if $schema->{'$dynamicAnchor'} !~ /^[A-Za-z_][A-Za-z0-9._-]*$/;
383 10         21 return 1;
384             }
385              
386 4     4   7 sub _eval_keyword_vocabulary ($data, $schema, $state) {
  4         7  
  4         6  
  4         6  
  4         5  
387 4         11 assert_keyword_type($state, $schema, 'object');
388              
389 4         15 foreach my $uri (sort keys $schema->{'$vocabulary'}->%*) {
390             abort({ %$state, _schema_path_suffix => $uri }, '$vocabulary value at "%s" is not a boolean', $uri)
391 4 50       12 if not is_type('boolean', $schema->{'$vocabulary'}{$uri});
392              
393 4         12 assert_uri($state, undef, $uri);
394             }
395              
396             abort($state, '$vocabulary can only appear at the schema resource root')
397 4 50       16 if length($state->{schema_path});
398              
399             abort($state, '$vocabulary can only appear at the document root')
400 4 50       18 if length($state->{traversed_schema_path}.$state->{schema_path});
401              
402 4         9 return 1;
403             }
404              
405 288     288   424 sub _eval_keyword_comment ($data, $schema, $state) {
  288         446  
  288         382  
  288         333  
  288         414  
406 288         754 assert_keyword_type($state, $schema, 'string');
407 288         676 return 1;
408             }
409              
410 150     150   519 sub _eval_keyword_definitions { goto \&_eval_keyword_defs }
411              
412 677     677   999 sub _eval_keyword_defs ($data, $schema, $state) {
  677         943  
  677         989  
  677         7940  
  677         934  
413 677         1629 assert_keyword_type($state, $schema, 'object');
414 675         1429 return 1;
415             }
416              
417 3706     3706   4450 sub _eval_keyword_type ($data, $schema, $state) {
  3706         4686  
  3706         4579  
  3706         3995  
  3706         4011  
418 3706 100       7888 if (ref $schema->{type} eq 'ARRAY') {
419 163 50       369 abort($state, 'type array is empty') if not $schema->{type}->@*;
420 163         374 foreach my $type ($schema->{type}->@*) {
421             abort($state, 'unrecognized type "%s"', $type//'')
422 344 100 50     461 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  1397   100     2491  
423             }
424 157 50       410 abort($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
425              
426 157         260 my $type = get_type($data);
427             return 1 if any {
428 292 100 100     2116 $type eq $_ or ($_ eq 'number' and $type eq 'integer')
      100        
      66        
      66        
      100        
      66        
      66        
      100        
      100        
429             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
430             and ($_ eq 'number' or ($_ eq 'integer' and $data == int($data))))
431             or ($_ eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR')
432 157 100       399 } $schema->{type}->@*;
433 92         361 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
434             }
435             else {
436 3543         7622 assert_keyword_type($state, $schema, 'string');
437             abort($state, 'unrecognized type "%s"', $schema->{type}//'')
438 3537 100 50     6191 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  16514   50     33066  
439              
440 3535         6073 my $type = get_type($data);
441             return 1 if $type eq $schema->{type} or ($schema->{type} eq 'number' and $type eq 'integer')
442             or ($type eq 'string' and $STRINGY_NUMBERS and looks_like_number($data)
443             and ($schema->{type} eq 'number' or ($schema->{type} eq 'integer' and $data == int($data))))
444 3535 100 100     18190 or ($schema->{type} eq 'boolean' and $SCALARREF_BOOLEANS and $type eq 'reference to SCALAR');
      100        
      100        
      66        
      66        
      66        
      66        
      100        
      100        
      66        
445 985         2502 return E($state, 'got %s, not %s', $type, $schema->{type});
446             }
447             }
448              
449 483     483   634 sub _eval_keyword_enum ($data, $schema, $state) {
  483         627  
  483         591  
  483         547  
  483         518  
450 483         1001 assert_keyword_type($state, $schema, 'array');
451              
452 483         664 my @s; my $idx = 0;
  483         641  
453 483 100       1049 return 1 if any { is_equal($data, $_, $s[$idx++] = {}) } $schema->{enum}->@*;
  933         10961  
454              
455             return E($state, 'value does not match'
456 219 100       1239 .(!(grep $_->{path}, @s) ? ''
457             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
458             }
459              
460 1055     1055   1448 sub _eval_keyword_const ($data, $schema, $state) {
  1055         1446  
  1055         1315  
  1055         1209  
  1055         1167  
461 1055 100       3160 return 1 if is_equal($data, $schema->{const}, my $s = {});
462 479 100       7372 return E($state, 'value does not match'.($s->{path} ? " (at '$s->{path}': $s->{error})" : ''));
463             }
464              
465 832     832   1035 sub _eval_keyword_multipleOf ($data, $schema, $state) {
  832         1273  
  832         1027  
  832         934  
  832         917  
466 832         1976 assert_keyword_type($state, $schema, 'number');
467 830 50       2151 abort($state, 'multipleOf value is not a positive number') if $schema->{multipleOf} <= 0;
468              
469             return 1 if not is_type('number', $data)
470             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data)
471 830 50 66     17942 and do { $data = 0+$data; 1 });
  2   66     8  
  2   33     7  
      100        
472              
473             # if either value is a float, use the bignum library for the calculation
474 640 100 100     1054 if (is_bignum($data) or is_bignum($schema->{multipleOf})
      66        
      100        
475             or get_type($data) eq 'number' or get_type($schema->{multipleOf}) eq 'number') {
476 52 100       83 $data = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
477 52 100       3197 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
478 52         1195 my ($quotient, $remainder) = $data->bdiv($divisor);
479 52 50       55972 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
480 52 100       428 return 1 if $remainder == 0;
481             }
482             else {
483 588         1326 my $quotient = $data / $schema->{multipleOf};
484 588 50       3889 return E($state, 'overflow while calculating quotient')
    50          
485             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf$/i;
486 588 100       1918 return 1 if int($quotient) == $quotient;
487             }
488              
489 297         7288 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
490             }
491              
492 585     585   877 sub _eval_keyword_maximum ($data, $schema, $state) {
  585         855  
  585         706  
  585         731  
  585         753  
493 585         1303 assert_keyword_type($state, $schema, 'number');
494 583 50 66     1048 return 1 if not is_type('number', $data)
      66        
      100        
495             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
496 388 100       1404 return 1 if 0+$data <= $schema->{maximum};
497 172         8395 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
498             }
499              
500 483     483   649 sub _eval_keyword_exclusiveMaximum ($data, $schema, $state) {
  483         650  
  483         610  
  483         498  
  483         552  
501 483         1090 assert_keyword_type($state, $schema, 'number');
502 481 50 66     788 return 1 if not is_type('number', $data)
      66        
      100        
503             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
504 292 100       917 return 1 if 0+$data < $schema->{exclusiveMaximum};
505 154         10670 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
506             }
507              
508 713     713   1004 sub _eval_keyword_minimum ($data, $schema, $state) {
  713         965  
  713         915  
  713         779  
  713         817  
509 713         1648 assert_keyword_type($state, $schema, 'number');
510 711 50 66     1248 return 1 if not is_type('number', $data)
      66        
      100        
511             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
512 507 100       1681 return 1 if 0+$data >= $schema->{minimum};
513 242         18449 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
514             }
515              
516 423     423   552 sub _eval_keyword_exclusiveMinimum ($data, $schema, $state) {
  423         637  
  423         557  
  423         480  
  423         487  
517 423         984 assert_keyword_type($state, $schema, 'number');
518 421 50 66     839 return 1 if not is_type('number', $data)
      66        
      100        
519             and not ($STRINGY_NUMBERS and is_type('string', $data) and looks_like_number($data));
520 232 100       858 return 1 if 0+$data > $schema->{exclusiveMinimum};
521 124         9792 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
522             }
523              
524 561     561   750 sub _eval_keyword_maxLength ($data, $schema, $state) {
  561         838  
  561         676  
  561         622  
  561         620  
525 561         1416 assert_non_negative_integer($schema, $state);
526              
527 561 100       934 return 1 if not is_type('string', $data);
528 352 100       1192 return 1 if length($data) <= $schema->{maxLength};
529 162         1908 return E($state, 'length is greater than %d', $schema->{maxLength});
530             }
531              
532 512     512   855 sub _eval_keyword_minLength ($data, $schema, $state) {
  512         777  
  512         728  
  512         609  
  512         590  
533 512         1355 assert_non_negative_integer($schema, $state);
534              
535 512 100       878 return 1 if not is_type('string', $data);
536 302 100       1178 return 1 if length($data) >= $schema->{minLength};
537 142         1949 return E($state, 'length is less than %d', $schema->{minLength});
538             }
539              
540 899     899   1148 sub _eval_keyword_pattern ($data, $schema, $state) {
  899         1314  
  899         1092  
  899         1048  
  899         961  
541 899         1944 assert_keyword_type($state, $schema, 'string');
542 899         2347 assert_pattern($state, $schema->{pattern});
543              
544 898 100       1420 return 1 if not is_type('string', $data);
545 671 100       5027 return 1 if $data =~ m/(?:$schema->{pattern})/;
546 313         703 return E($state, 'pattern does not match');
547             }
548              
549 425     425   563 sub _eval_keyword_maxItems ($data, $schema, $state) {
  425         513  
  425         519  
  425         495  
  425         484  
550 425         977 assert_non_negative_integer($schema, $state);
551              
552 425 100       651 return 1 if not is_type('array', $data);
553 256 100       703 return 1 if @$data <= $schema->{maxItems};
554 122 100       1906 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
555             }
556              
557 424     424   578 sub _eval_keyword_minItems ($data, $schema, $state) {
  424         616  
  424         547  
  424         533  
  424         503  
558 424         1056 assert_non_negative_integer($schema, $state);
559              
560 424 100       682 return 1 if not is_type('array', $data);
561 257 100       791 return 1 if @$data >= $schema->{minItems};
562 124 100       1931 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
563             }
564              
565 775     775   1060 sub _eval_keyword_uniqueItems ($data, $schema, $state) {
  775         1014  
  775         1036  
  775         929  
  775         828  
566 775         1682 assert_keyword_type($state, $schema, 'boolean');
567 775 100       1438 return 1 if not is_type('array', $data);
568 614 100       2446 return 1 if not $schema->{uniqueItems};
569 449 100       3679 return 1 if is_elements_unique($data, my $equal_indices = []);
570 207         535 return E($state, 'items at indices %d and %d are not unique', @$equal_indices);
571             }
572              
573 84     84   110 sub _eval_keyword_maxContains ($data, $schema, $state) {
  84         99  
  84         82  
  84         81  
  84         83  
574 84         189 assert_non_negative_integer($schema, $state);
575 84 100       183 return 1 if not exists $state->{_num_contains};
576 76 50       114 return 1 if not is_type('array', $data);
577              
578             return E($state, 'contains too many matching items')
579 76 100       213 if $state->{_num_contains} > $schema->{maxContains};
580              
581 44         1040 return 1;
582             }
583              
584 102     102   119 sub _eval_keyword_minContains ($data, $schema, $state) {
  102         126  
  102         135  
  102         99  
  102         102  
585 102         222 assert_non_negative_integer($schema, $state);
586 102 100       260 return 1 if not exists $state->{_num_contains};
587 94 50       170 return 1 if not is_type('array', $data);
588              
589             return E($state, 'contains too few matching items')
590 94 100       261 if $state->{_num_contains} < $schema->{minContains};
591              
592 60         1024 return 1;
593             }
594              
595 340     340   410 sub _eval_keyword_maxProperties ($data, $schema, $state) {
  340         460  
  340         414  
  340         376  
  340         384  
596 340         750 assert_non_negative_integer($schema, $state);
597              
598 340 100       559 return 1 if not is_type('object', $data);
599 202 100       745 return 1 if keys %$data <= $schema->{maxProperties};
600             return E($state, 'more than %d propert%s', $schema->{maxProperties},
601 98 100       1994 $schema->{maxProperties} > 1 ? 'ies' : 'y');
602             }
603              
604 340     340   459 sub _eval_keyword_minProperties ($data, $schema, $state) {
  340         481  
  340         432  
  340         406  
  340         419  
605 340         866 assert_non_negative_integer($schema, $state);
606              
607 340 100       684 return 1 if not is_type('object', $data);
608 202 100       742 return 1 if keys %$data >= $schema->{minProperties};
609             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
610 98 100       1714 $schema->{minProperties} > 1 ? 'ies' : 'y');
611             }
612              
613 1414     1414   1586 sub _eval_keyword_required ($data, $schema, $state) {
  1414         1694  
  1414         1547  
  1414         1460  
  1414         1355  
614 1414         2617 assert_keyword_type($state, $schema, 'array');
615             abort($state, '"required" element is not a string')
616 1414 50       3005 if any { !is_type('string', $_) } $schema->{required}->@*;
  1600         2581  
617 1414 50       3250 abort($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
618              
619 1414 100       2101 return 1 if not is_type('object', $data);
620              
621 1264         4266 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
622 1264 100       2671 return 1 if not @missing;
623 566 100       2177 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
624             }
625              
626 271     271   355 sub _eval_keyword_dependentRequired ($data, $schema, $state) {
  271         335  
  271         313  
  271         379  
  271         311  
627 271         558 assert_keyword_type($state, $schema, 'object');
628              
629 271         1032 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
630             E({ %$state, _schema_path_suffix => $property }, 'value is not an array'), next
631 287 50       610 if not is_type('array', $schema->{dependentRequired}{$property});
632              
633 287         880 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
634             abort({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
635 301 100       898 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
636             }
637              
638             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
639 286 50       783 if not is_elements_unique($schema->{dependentRequired}{$property});
640             }
641              
642 270 100       445 return 1 if not is_type('object', $data);
643              
644 173         231 my $valid = 1;
645 173         403 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
646 189 100       430 next if not exists $data->{$property};
647              
648 153 100       704 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
649 79 100       735 $valid = E({ %$state, _schema_path_suffix => $property },
650             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
651             }
652             }
653              
654 173 100       470 return 1 if $valid;
655 79         147 return E($state, 'not all dependencies are satisfied');
656             }
657              
658 575     575   820 sub _eval_keyword_allOf ($data, $schema, $state) {
  575         823  
  575         730  
  575         680  
  575         653  
659 575         1594 assert_array_schemas($schema, $state);
660              
661 575         742 my @invalid;
662 575         1940 foreach my $idx (0..$schema->{allOf}->$#*) {
663             next if _eval_subschema($data, $schema->{allOf}[$idx],
664 843 100       8059 +{ %$state, schema_path => $state->{schema_path}.'/allOf/'.$idx });
665              
666 209         827 push @invalid, $idx;
667 209 100       560 last if $state->{short_circuit};
668             }
669              
670 404 100       1402 return 1 if @invalid == 0;
671              
672 169         289 my $pl = @invalid > 1;
673 169 100       679 return E($state, 'subschema%s %s %s not valid', $pl?'s':'', join(', ', @invalid), $pl?'are':'is');
    100          
674             }
675              
676 433     433   626 sub _eval_keyword_anyOf ($data, $schema, $state) {
  433         583  
  433         509  
  433         507  
  433         479  
677 433         1089 assert_array_schemas($schema, $state);
678              
679 433         540 my $valid = 0;
680 433         587 my @errors;
681 433         1390 foreach my $idx (0..$schema->{anyOf}->$#*) {
682             next if not _eval_subschema($data, $schema->{anyOf}[$idx],
683 760 100       7067 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/anyOf/'.$idx });
684 239         1132 ++$valid;
685 239 100       607 last if $state->{short_circuit};
686             }
687              
688 294 100       819 return 1 if $valid;
689 92         251 push $state->{errors}->@*, @errors;
690 92         175 return E($state, 'no subschemas are valid');
691             }
692              
693 509     509   663 sub _eval_keyword_oneOf ($data, $schema, $state) {
  509         607  
  509         593  
  509         559  
  509         492  
694 509         1274 assert_array_schemas($schema, $state);
695              
696 509         696 my (@valid, @errors);
697 509         1645 foreach my $idx (0..$schema->{oneOf}->$#*) {
698             next if not _eval_subschema($data, $schema->{oneOf}[$idx],
699 1061 100       9330 +{ %$state, errors => \@errors, schema_path => $state->{schema_path}.'/oneOf/'.$idx });
700 377         1720 push @valid, $idx;
701 377 100 100     1275 last if @valid > 1 and $state->{short_circuit};
702             }
703              
704 358 100       1080 return 1 if @valid == 1;
705              
706 201 100       360 if (not @valid) {
707 123         308 push $state->{errors}->@*, @errors;
708 123         249 return E($state, 'no subschemas are valid');
709             }
710             else {
711 78         288 return E($state, 'multiple subschemas are valid: '.join(', ', @valid));
712             }
713             }
714              
715 293     293   373 sub _eval_keyword_not ($data, $schema, $state) {
  293         471  
  293         354  
  293         326  
  293         319  
716 293 100 66     783 return !$schema->{not} || E($state, 'subschema is true') if is_type('boolean', $schema->{not});
717              
718             return 1 if not _eval_subschema($data, $schema->{not},
719 181 100       1839 +{ %$state, schema_path => $state->{schema_path}.'/not', short_circuit => 1, errors => [] });
720              
721 135         555 return E($state, 'subschema is valid');
722             }
723              
724 326     326   439 sub _eval_keyword_if ($data, $schema, $state) {
  326         466  
  326         385  
  326         377  
  326         338  
725 326 100 100     1068 return 1 if not exists $schema->{then} and not exists $schema->{else};
726             my $keyword = _eval_subschema($data, $schema->{if},
727 282 100       2501 +{ %$state, schema_path => $state->{schema_path}.'/if', short_circuit => 1, errors => [] })
728             ? 'then' : 'else';
729              
730 282 100       1586 return 1 if not exists $schema->{$keyword};
731              
732             return $schema->{$keyword} || E({ %$state, keyword => $keyword }, 'subschema is false')
733 224 100 66     514 if is_type('boolean', $schema->{$keyword});
734              
735             return 1 if _eval_subschema($data, $schema->{$keyword},
736 192 100       1396 +{ %$state, schema_path => $state->{schema_path}.'/'.$keyword });
737 62         519 return E({ %$state, keyword => $keyword }, 'subschema is not valid');
738             }
739              
740 337     337   504 sub _eval_keyword_dependentSchemas ($data, $schema, $state) {
  337         439  
  337         372  
  337         371  
  337         359  
741 337         714 assert_keyword_type($state, $schema, 'object');
742              
743 337 100       585 return 1 if not is_type('object', $data);
744              
745 213         291 my $valid = 1;
746 213         720 foreach my $property (sort keys $schema->{dependentSchemas}->%*) {
747             next if not exists $data->{$property}
748             or _eval_subschema($data, $schema->{dependentSchemas}{$property},
749 263 100 100     1023 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependentSchemas', $property) });
750              
751 97         320 $valid = 0;
752 97 100       277 last if $state->{short_circuit};
753             }
754              
755 213 100       558 return E($state, 'not all dependencies are satisfied') if not $valid;
756 116         219 return 1;
757             }
758              
759 186     186   246 sub _eval_keyword_dependencies ($data, $schema, $state) {
  186         312  
  186         234  
  186         235  
  186         221  
760 186         473 assert_keyword_type($state, $schema, 'object');
761              
762 186 100       347 return 1 if not is_type('object', $data);
763              
764 119         176 my $valid = 1;
765 119         421 foreach my $property (sort keys $schema->{dependencies}->%*) {
766 166 100       353 if (is_type('array', $schema->{dependencies}{$property})) {
767             # as in dependentRequired
768              
769 52         120 foreach my $index (0..$schema->{dependencies}{$property}->$#*) {
770             $valid = E({ %$state, _schema_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
771 62 50       111 if not is_type('string', $schema->{dependencies}{$property}[$index]);
772             }
773              
774             abort({ %$state, _schema_path_suffix => $property }, 'elements are not unique')
775 52 50       114 if not is_elements_unique($schema->{dependencies}{$property});
776              
777 52 100       107 next if not exists $data->{$property};
778              
779 24 100       124 if (my @missing = grep !exists($data->{$_}), $schema->{dependencies}{$property}->@*) {
780 14 100       120 $valid = E({ %$state, _schema_path_suffix => $property },
781             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
782             }
783             }
784             else {
785             # as in dependentSchemas
786             next if not exists $data->{$property}
787             or _eval_subschema($data, $schema->{dependencies}{$property},
788 114 100 100     567 +{ %$state, schema_path => jsonp($state->{schema_path}, 'dependencies', $property) });
789              
790 47         183 $valid = 0;
791 47 100       136 last if $state->{short_circuit};
792             }
793             }
794              
795 119 100       362 return 1 if $valid;
796 59         96 return E($state, 'not all dependencies are satisfied');
797             }
798              
799 411     411   582 sub _eval_keyword_prefixItems ($data, $schema, $state) {
  411         499  
  411         513  
  411         466  
  411         482  
800 411 50       857 return if not assert_array_schemas($schema, $state);
801 411         1261 goto \&_eval_keyword__items_array_schemas;
802             }
803              
804 1304     1304   1680 sub _eval_keyword_items ($data, $schema, $state) {
  1304         1732  
  1304         1534  
  1304         1442  
  1304         1469  
805 1304 100       3566 if (ref $schema->{items} eq 'ARRAY') {
806             abort($state, 'array form of "items" not supported in %s', $state->{spec_version})
807 700 100 100     1858 if ($state->{spec_version}//'') eq 'draft2020-12';
808              
809 699         2341 goto \&_eval_keyword__items_array_schemas;
810             }
811              
812 604   100     2543 $state->{_last_items_index} //= -1;
813 604         1876 goto \&_eval_keyword__items_schema;
814             }
815              
816 219     219   246 sub _eval_keyword_additionalItems ($data, $schema, $state) {
  219         266  
  219         246  
  219         217  
  219         214  
817 219 100       469 return 1 if not exists $state->{_last_items_index};
818 191         550 goto \&_eval_keyword__items_schema;
819             }
820              
821             # prefixItems (draft 2020-12), array-based items (all drafts)
822 1110     1110   1421 sub _eval_keyword__items_array_schemas ($data, $schema, $state) {
  1110         1306  
  1110         1322  
  1110         1271  
  1110         1176  
823 1110 50       2720 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
824 1110 100       2020 return 1 if not is_type('array', $data);
825              
826 897         1213 my $valid = 1;
827              
828 897         2420 foreach my $idx (0..$data->$#*) {
829 1581 100       4801 last if $idx > $schema->{$state->{keyword}}->$#*;
830 1294         2917 $state->{_last_items_index} = $idx;
831              
832 1294 100       2865 if (is_type('boolean', $schema->{$state->{keyword}}[$idx])) {
833 286 100       1050 next if $schema->{$state->{keyword}}[$idx];
834 108         1814 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx,
835             _schema_path_suffix => $idx }, 'item not permitted');
836             }
837             else {
838             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}}[$idx],
839             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
840 1008 100       10350 schema_path => $state->{schema_path}.'/'.$state->{keyword}.'/'.$idx });
841             }
842              
843 175         668 $valid = 0;
844             last if $state->{short_circuit} and not exists $schema->{
845             $state->{keyword} eq 'prefixItems' ? 'items'
846 175 50 100     986 : $state->{keyword} eq 'items' ? 'additionalItems' : die
    100          
    100          
847             };
848             }
849              
850 897 100       1931 return E($state, 'not all items are valid') if not $valid;
851 725         1431 return 1;
852             }
853              
854             # schema-based items (all drafts), and additionalItems (drafts 4,6,7,2019-09)
855 795     795   957 sub _eval_keyword__items_schema ($data, $schema, $state) {
  795         969  
  795         820  
  795         840  
  795         901  
856 795 100       1374 return 1 if not is_type('array', $data);
857 691 100       1935 return 1 if $state->{_last_items_index} == $data->$#*;
858              
859 447         546 my $valid = 1;
860 447         1496 foreach my $idx ($state->{_last_items_index}+1 .. $data->$#*) {
861 676 100 100     1684 if (is_type('boolean', $schema->{$state->{keyword}})
862             and ($state->{keyword} eq 'additionalItems')) {
863 32 100       170 next if $schema->{$state->{keyword}};
864             $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
865             '%sitem not permitted',
866 26 50 33     498 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '');
867             }
868             else {
869             next if _eval_subschema($data->[$idx], $schema->{$state->{keyword}},
870             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
871 644 100       6330 schema_path => $state->{schema_path}.'/'.$state->{keyword} });
872 219         710 $valid = 0;
873             }
874              
875 245 100       745 last if $state->{short_circuit};
876             }
877              
878 382         941 $state->{_last_items_index} = $data->$#*;
879              
880             return E($state, 'subschema is not valid against all %sitems',
881 382 100 100     1290 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '')
    100          
882             if not $valid;
883 179         329 return 1;
884             }
885              
886 717     717   903 sub _eval_keyword_contains ($data, $schema, $state) {
  717         973  
  717         867  
  717         863  
  717         788  
887 717 100       1458 return 1 if not is_type('array', $data);
888              
889 504         1463 $state->{_num_contains} = 0;
890 504         649 my @errors;
891 504         1324 foreach my $idx (0..$data->$#*) {
892 622 100       7680 if (_eval_subschema($data->[$idx], $schema->{contains},
893             +{ %$state, errors => \@errors,
894             data_path => $state->{data_path}.'/'.$idx,
895             schema_path => $state->{schema_path}.'/contains' })) {
896 390         2025 ++$state->{_num_contains};
897              
898             last if $state->{short_circuit}
899             and (not exists $schema->{maxContains} or $state->{_num_contains} > $schema->{maxContains})
900 390 100 100     2678 and ($state->{_num_contains} >= ($schema->{minContains}//1));
      100        
      100        
      100        
901             }
902             }
903              
904             # note: no items contained is only valid when minContains is explicitly 0
905 504 100 66     4606 if (not $state->{_num_contains} and (($schema->{minContains}//1) > 0
      66        
906             or $state->{spec_version} and $state->{spec_version} eq 'draft7')) {
907 195         429 push $state->{errors}->@*, @errors;
908 195         419 return E($state, 'subschema is not valid against any item');
909             }
910              
911 309         788 return 1;
912             }
913              
914 2401     2401   3007 sub _eval_keyword_properties ($data, $schema, $state) {
  2401         3082  
  2401         2721  
  2401         2723  
  2401         2592  
915 2401         5151 assert_keyword_type($state, $schema, 'object');
916 2401 100       3921 return 1 if not is_type('object', $data);
917              
918 2150         2719 my $valid = 1;
919 2150         7159 foreach my $property (sort keys $schema->{properties}->%*) {
920 2714 100       5746 next if not exists $data->{$property};
921              
922 1670 100       3711 if (is_type('boolean', $schema->{properties}{$property})) {
923 323 100       1293 next if $schema->{properties}{$property};
924 106         1006 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
925             _schema_path_suffix => $property }, 'property not permitted');
926             }
927             else {
928             next if _eval_subschema($data->{$property}, $schema->{properties}{$property},
929             +{ %$state,
930             data_path => jsonp($state->{data_path}, $property),
931 1347 100       6452 schema_path => jsonp($state->{schema_path}, 'properties', $property) });
932              
933 329         1091 $valid = 0;
934             }
935 435 100       1470 last if $state->{short_circuit};
936             }
937              
938 1995 100       5020 return E($state, 'not all properties are valid') if not $valid;
939 1582         3157 return 1;
940             }
941              
942 809     809   1080 sub _eval_keyword_patternProperties ($data, $schema, $state) {
  809         1110  
  809         988  
  809         915  
  809         912  
943 809         1813 assert_keyword_type($state, $schema, 'object');
944              
945 809         3126 foreach my $property (sort keys $schema->{patternProperties}->%*) {
946 1250         7717 assert_pattern({ %$state, _schema_path_suffix => $property }, $property);
947             }
948              
949 807 100       1471 return 1 if not is_type('object', $data);
950              
951 614         963 my $valid = 1;
952 614         1900 foreach my $property_pattern (sort keys $schema->{patternProperties}->%*) {
953 898         10041 foreach my $property (sort grep m/(?:$property_pattern)/, keys %$data) {
954 557 100       1518 if (is_type('boolean', $schema->{patternProperties}{$property_pattern})) {
955 319 100       1353 next if $schema->{patternProperties}{$property_pattern};
956 108         1057 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
957             _schema_path_suffix => $property_pattern }, 'property not permitted');
958             }
959             else {
960             next if _eval_subschema($data->{$property}, $schema->{patternProperties}{$property_pattern},
961             +{ %$state,
962             data_path => jsonp($state->{data_path}, $property),
963 238 100       1071 schema_path => jsonp($state->{schema_path}, 'patternProperties', $property_pattern) });
964              
965 87         302 $valid = 0;
966             }
967 195 100       852 last if $state->{short_circuit};
968             }
969             }
970              
971 614 100       2770 return E($state, 'not all properties are valid') if not $valid;
972 434         978 return 1;
973             }
974              
975 755     755   980 sub _eval_keyword_additionalProperties ($data, $schema, $state) {
  755         940  
  755         847  
  755         835  
  755         789  
976 755 100       1279 return 1 if not is_type('object', $data);
977              
978 556         770 my $valid = 1;
979 556         1493 foreach my $property (sort keys %$data) {
980 552 100 100     1602 next if exists $schema->{properties} and exists $schema->{properties}{$property};
981             next if exists $schema->{patternProperties}
982 438 100 100     1209 and any { $property =~ /(?:$_)/ } keys $schema->{patternProperties}->%*;
  148         1361  
983              
984 350 100       722 if (is_type('boolean', $schema->{additionalProperties})) {
985 192 100       756 next if $schema->{additionalProperties};
986              
987 172         1662 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
988             'additional property not permitted');
989             }
990             else {
991             next if _eval_subschema($data->{$property}, $schema->{additionalProperties},
992             +{ %$state,
993             data_path => jsonp($state->{data_path}, $property),
994 158 100       699 schema_path => $state->{schema_path}.'/additionalProperties' });
995              
996 43         144 $valid = 0;
997             }
998 215 100       1023 last if $state->{short_circuit};
999             }
1000              
1001 504 100       1148 return E($state, 'not all additional properties are valid') if not $valid;
1002 290         567 return 1;
1003             }
1004              
1005 463     463   810 sub _eval_keyword_propertyNames ($data, $schema, $state) {
  463         665  
  463         589  
  463         508  
  463         550  
1006 463 100       870 return 1 if not is_type('object', $data);
1007              
1008 288         385 my $valid = 1;
1009 288         841 foreach my $property (sort keys %$data) {
1010             next if _eval_subschema($property, $schema->{propertyNames},
1011             +{ %$state,
1012             data_path => jsonp($state->{data_path}, $property),
1013 202 100       899 schema_path => $state->{schema_path}.'/propertyNames' });
1014              
1015 116         451 $valid = 0;
1016 116 100       343 last if $state->{short_circuit};
1017             }
1018              
1019 288 100       709 return E($state, 'not all property names are valid') if not $valid;
1020 172         329 return 1;
1021             }
1022              
1023 384     384   532 sub _eval_keyword_unevaluatedItems ($data, $schema, $state) {
  384         573  
  384         475  
  384         455  
  384         500  
1024 384         879 abort($state, 'keyword not yet supported');
1025             }
1026              
1027 584     584   789 sub _eval_keyword_unevaluatedProperties ($data, $schema, $state) {
  584         798  
  584         738  
  584         665  
  584         598  
1028 584         1290 abort($state, 'keyword not yet supported');
1029             }
1030              
1031             # UTILITIES
1032              
1033             # supports the six core types, plus integer (which is also a number)
1034             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1035             # note that sometimes a value may return true for more than one type, e.g. integer+number,
1036             # or number+string, depending on its internal flags.
1037             # copied from JSON::Schema::Modern::Utilities::is_type
1038 52894     52894 0 952525 sub is_type ($type, $value) {
  52894         57434  
  52894         59891  
  52894         53607  
1039 52894 100       82257 if ($type eq 'null') {
1040 83         380 return !(defined $value);
1041             }
1042 52811 100       80310 if ($type eq 'boolean') {
1043 6022         10008 return is_bool($value);
1044             }
1045 46789 100       68360 if ($type eq 'object') {
1046 12088         32097 return ref $value eq 'HASH';
1047             }
1048 34701 100       51866 if ($type eq 'array') {
1049 8790         24427 return ref $value eq 'ARRAY';
1050             }
1051              
1052 25911 100 100     60795 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
1053 25895 100       39249 return 0 if not defined $value;
1054 25877         95788 my $flags = B::svref_2object(\$value)->FLAGS;
1055              
1056             # dualvars with the same string and (stringified) numeric value could be either a string or a
1057             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
1058             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
1059             # numified strings do have POK set, so we can tell which one came first.
1060              
1061 25877 100       50806 if ($type eq 'string') {
1062             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
1063             return !length ref($value)
1064             && $flags & B::SVf_POK
1065             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
1066 17   100 17   174081 || do { no warnings 'numeric'; 0+$value eq $value });
  17         36  
  17         7575  
  16845         97614  
1067             }
1068              
1069 9032 100       14787 if ($type eq 'number') {
1070             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1071 6143   100     9725 return is_bignum($value) || created_as_number($value);
1072             }
1073              
1074 2889 50       5720 if ($type eq 'integer') {
1075             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
1076             # therefore they will fail this check
1077 2889   100     5079 return is_bignum($value) && $value->is_int
1078             # if dualvar, PV and stringified NV/IV must be identical
1079             || created_as_number($value) && int($value) == $value;
1080             }
1081             }
1082              
1083 16 100       92 if ($type =~ /^reference to (.+)$/) {
1084 11   33     114 return !blessed($value) && ref($value) eq $1;
1085             }
1086              
1087 5         24 return ref($value) eq $type;
1088             }
1089              
1090             # returns one of the six core types, plus integer
1091             # we do NOT check $STRINGY_NUMBERS here -- you must do that in the caller
1092             # copied from JSON::Schema::Modern::Utilities::get_type
1093 35088     35088 0 743994 sub get_type ($value) {
  35088         41210  
  35088         36719  
1094 35088 100       77573 return 'object' if ref $value eq 'HASH';
1095 13041 100       20572 return 'boolean' if is_bool($value);
1096 11062 100       23534 return 'null' if not defined $value;
1097 10764 100       17920 return 'array' if ref $value eq 'ARRAY';
1098              
1099             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1100 9481 100       15609 if (length(my $ref = ref $value)) {
1101 455 100       1818 return $ref eq 'Math::BigInt' ? 'integer'
    100          
    100          
    100          
1102             : $ref eq 'Math::BigFloat' ? ($value->is_int ? 'integer' : 'number')
1103             : (defined blessed($value) ? '' : 'reference to ').$ref;
1104             }
1105              
1106 9026         23386 my $flags = B::svref_2object(\$value)->FLAGS;
1107              
1108             # dualvars with the same string and (stringified) numeric value could be either a string or a
1109             # number, and before 5.36 we can't tell the difference, so we choose number because it has been
1110             # evaluated as a number already.
1111             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
1112             # numified strings do have POK set, so we can tell which one came first.
1113              
1114             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
1115             return 'string'
1116             if $flags & B::SVf_POK
1117             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
1118 17 100 100 17   105 || do { no warnings 'numeric'; 0+$value eq $value });
  17   100     43  
  17         2459  
  9026         25707  
1119              
1120             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
1121             # therefore they will fail this check
1122 4387 100       16503 return int($value) == $value ? 'integer' : 'number' if created_as_number($value);
    100          
1123              
1124             # this might be a scalar with POK|IOK or POK|NOK set
1125 15         48 return 'ambiguous type';
1126             }
1127              
1128             # lifted from JSON::MaybeXS
1129             # note: unlike builtin::compat::is_bool on older perls, we do not accept
1130             # dualvar(0,"") or dualvar(1,"1") because JSON::PP and Cpanel::JSON::XS
1131             # do not encode these as booleans.
1132 17     17   103 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  17         34  
  17         1524  
1133 17     17   95 use if HAVE_BUILTIN, experimental => 'builtin';
  17         132  
  17         543  
1134 19063     19063 0 19820 sub is_bool ($value) {
  19063         21399  
  19063         19637  
1135 19063 50 66     74438 HAVE_BUILTIN and builtin::is_bool($value)
      66        
1136             or
1137             !!blessed($value)
1138             and ($value->isa('JSON::PP::Boolean')
1139             or $value->isa('Cpanel::JSON::XS::Boolean')
1140             or $value->isa('JSON::XS::Boolean'));
1141             }
1142              
1143 11375     11375 0 12380 sub is_bignum ($value) {
  11375         13548  
  11375         11760  
1144 11375         59871 ref($value) =~ /^Math::Big(?:Int|Float)$/;
1145             }
1146              
1147             # compares two arbitrary data payloads for equality, as per
1148             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
1149             # $state hashref supports the following fields/configs:
1150             # - path: location of the first difference
1151             # - error: description of the difference
1152             # - $SCALARREF_BOOLEANS: treats \0 and \1 as boolean values
1153             # - $STRINGY_NUMBERS: strings will be typed as numbers if looks_like_number() is true
1154             # copied from JSON::Schema::Modern::Utilities::is_equal
1155 4171     4171 0 4999 sub is_equal ($x, $y, $state = {}) {
  4171         5072  
  4171         4901  
  4171         5012  
  4171         4470  
1156 4171   100     14629 $state->{path} //= '';
1157              
1158 4171         8013 my @types = map get_type($_), $x, $y;
1159              
1160 4171 100       11456 $state->{error} = 'ambiguous type encountered', return 0
1161             if grep $types[$_] eq 'ambiguous type', 0..1;
1162              
1163 4168 100       7210 if ($SCALARREF_BOOLEANS) {
1164 99 100       158 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
1165 99 100       155 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
1166             }
1167              
1168 4168 100       6233 if ($STRINGY_NUMBERS) {
1169 18 100 100     111 ($x, $types[0]) = (0+$x, int(0+$x) == $x ? 'integer' : 'number')
    100          
1170             if $types[0] eq 'string' and looks_like_number($x);
1171              
1172 18 100 100     67 ($y, $types[1]) = (0+$y, int(0+$y) == $y ? 'integer' : 'number')
    100          
1173             if $types[1] eq 'string' and looks_like_number($y);
1174             }
1175              
1176 4168 100       9441 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
1177 3325 100       5346 return 1 if $types[0] eq 'null';
1178 3311 100 100     10192 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
1179             if $types[0] eq 'string';
1180 1778 100 100     7381 ($x == $y and return 1), $state->{error} = "$types[0]s not equal", return 0
1181             if grep $types[0] eq $_, qw(boolean number integer);
1182              
1183 623         1003 my $path = $state->{path};
1184 623 100       1009 if ($types[0] eq 'object') {
1185 217 100       674 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
1186             if keys %$x != keys %$y;
1187              
1188 200 100       1060 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
1189 7         25 my $pos = substr($s->{path}, 1);
1190 7         28 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
1191 7         37 return 0;
1192             }
1193              
1194 193         606 foreach my $property (sort keys %$x) {
1195 231         449 $state->{path} = jsonp($path, $property);
1196 231 100       541 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
1197             }
1198              
1199 106         643 return 1;
1200             }
1201              
1202 406 50       705 if ($types[0] eq 'array') {
1203 406 100       766 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
1204 397         856 foreach my $idx (0 .. $x->$#*) {
1205 441         1050 $state->{path} = $path.'/'.$idx;
1206 441 100       1138 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
1207             }
1208 269         5204 return 1;
1209             }
1210              
1211 0         0 $state->{error} = 'uh oh', return 0; # should never get here
1212             }
1213              
1214             # checks array elements for uniqueness. short-circuits on first pair of matching elements
1215             # if second arrayref is provided, it is populated with the indices of identical items
1216             # supports the following configs:
1217             # - $SCALARREF_BOOLEANS: treats \0 and \1 as boolean values
1218             # - $STRINGY_NUMBERS: strings will be typed as numbers if looks_like_number() is true
1219             # copied from JSON::Schema::Modern::Utilities::is_elements_unique
1220 2358     2358 0 2825 sub is_elements_unique ($array, $equal_indices = undef) {
  2358         2719  
  2358         3089  
  2358         2596  
1221 2358         5886 foreach my $idx0 (0 .. $array->$#*-1) {
1222 846         2025 foreach my $idx1 ($idx0+1 .. $array->$#*) {
1223 1251 100       3151 if (is_equal($array->[$idx0], $array->[$idx1])) {
1224 207 50       2435 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
1225 207         605 return 0;
1226             }
1227             }
1228             }
1229 2151         4918 return 1;
1230             }
1231              
1232             # shorthand for creating and appending json pointers
1233             # the first argument is a json pointer; remaining arguments are path segments to be encoded and
1234             # appended
1235             # copied from JSON::Schema::Modern::Utilities::jsonp
1236             sub jsonp {
1237 24020     24020 0 162989 return join('/', shift, map s/~/~0/gr =~ s!/!~1!gr, grep defined, @_);
1238             }
1239              
1240             # shorthand for finding the canonical uri of the present schema location
1241             # copied from JSON::Schema::Modern::Utilities::canonical_uri
1242 30612     30612 0 33939 sub canonical_uri ($state, @extra_path) {
  30612         33794  
  30612         35887  
  30612         33363  
1243 30612 100 100     94003 return $state->{initial_schema_uri} if not @extra_path and not length($state->{schema_path});
1244 16601         48107 my $uri = $state->{initial_schema_uri}->clone;
1245 16601 100 100     1047002 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{schema_path}, @extra_path) : $state->{schema_path});
1246 16601 100       60807 undef $fragment if not length($fragment);
1247 16601         33245 $uri->fragment($fragment);
1248 16601         82288 $uri;
1249             }
1250              
1251             # shorthand for creating error objects
1252             # based on JSON::Schema::Modern::Utilities::E
1253 9843     9843 0 28723 sub E ($state, $error_string, @args) {
  9843         11245  
  9843         12469  
  9843         13218  
  9843         9760  
1254             # sometimes the keyword shouldn't be at the very end of the schema path
1255 9843         18835 my $sps = delete $state->{_schema_path_suffix};
1256 9843 100 100     35490 my @schema_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
1257              
1258 9843         19875 my $uri = canonical_uri($state, $state->{keyword}, @schema_path_suffix);
1259              
1260             my $keyword_location = $state->{traversed_schema_path}
1261 9843         23204 .jsonp($state->{schema_path}, $state->{keyword}, @schema_path_suffix);
1262              
1263 9843 100 100     23496 undef $uri if $uri eq '' and $keyword_location eq ''
      100        
      100        
      100        
1264             or ($uri->fragment//'') eq $keyword_location and $uri->clone->fragment(undef) eq '';
1265              
1266             push $state->{errors}->@*, {
1267             instanceLocation => $state->{data_path},
1268 9843 100       2622049 keywordLocation => $keyword_location,
    100          
1269             defined $uri ? ( absoluteKeywordLocation => $uri->to_string) : (),
1270             error => @args ? sprintf($error_string, @args) : $error_string,
1271             };
1272              
1273 9843         211708 return 0;
1274             }
1275              
1276             # creates an error object, but also aborts evaluation immediately
1277             # only this error is returned, because other errors on the stack might not actually be "real"
1278             # errors (consider if we were in the middle of evaluating a "not" or "if")
1279 1674     1674 0 236566 sub abort ($state, $error_string, @args) {
  1674         2101  
  1674         2102  
  1674         2188  
  1674         1884  
1280 1674         4191 E($state, $error_string, @args);
1281 1674         27777 die pop $state->{errors}->@*;
1282             }
1283              
1284             # one common usecase of abort()
1285 28018     28018 0 31009 sub assert_keyword_type ($state, $schema, $type) {
  28018         29882  
  28018         29108  
  28018         32945  
  28018         28067  
1286 28018 100       65426 return 1 if is_type($type, $schema->{$state->{keyword}});
1287 18 100       140 abort($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
1288             }
1289              
1290 2149     2149 0 2624 sub assert_pattern ($state, $pattern) {
  2149         2549  
  2149         2602  
  2149         2306  
1291 2149         3110 try {
1292 2149     0   12000 local $SIG{__WARN__} = sub { die @_ };
  0         0  
1293 2149         24615 qr/$pattern/;
1294             }
1295 3         10 catch ($e) { abort($state, $e); }
1296 2146         6751 return 1;
1297             }
1298              
1299             # based on JSON::Schema::Modern::Utilities::assert_uri_reference
1300 2421     2421 0 2905 sub assert_uri_reference ($state, $schema) {
  2421         2716  
  2421         2851  
  2421         2479  
1301 2421         4380 my $string = $schema->{$state->{keyword}};
1302             abort($state, '%s value is not a valid URI reference', $state->{keyword})
1303             # see also uri-reference format sub
1304 2421 50 33     6479 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      66        
      33        
1305             or $string =~ /[^[:ascii:]]/ # ascii characters only
1306             or $string =~ /#/ # no fragment, except...
1307             and $string !~ m{#$} # allow empty fragment
1308             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # allow plain-name fragment
1309             and $string !~ m{#/(?:[^~]|~[01])*$}; # allow json pointer fragment
1310              
1311 2421         677951 return 1;
1312             }
1313              
1314             # based on JSON::Schema::Modern::Utilities::assert_uri
1315 5648     5648 0 5955 sub assert_uri ($state, $schema, $override = undef) {
  5648         6095  
  5648         6357  
  5648         6963  
  5648         5563  
1316 5648   66     17163 my $string = $override // $schema->{$state->{keyword}};
1317 5648         14966 my $uri = Mojo::URL->new($string);
1318              
1319 5648 0 33     386606 abort($state, '"%s" is not a valid URI', $string)
      33        
      66        
      33        
      33        
      33        
1320             # see also uri format sub
1321             if fc($uri->to_unsafe_string) ne fc($string)
1322             or $string =~ /[^[:ascii:]]/ # ascii characters only
1323             or not $uri->is_abs # must have a schema
1324             or $string =~ /#/ # no fragment, except...
1325             and $string !~ m{#$} # empty fragment
1326             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*$} # plain-name fragment
1327             and $string !~ m{#/(?:[^~]|~[01])*$}; # json pointer fragment
1328              
1329 5648         915798 return 1;
1330             }
1331              
1332 2788     2788 0 3532 sub assert_non_negative_integer ($schema, $state) {
  2788         3404  
  2788         3409  
  2788         3155  
1333 2788         5967 assert_keyword_type($state, $schema, 'integer');
1334             abort($state, '%s value is not a non-negative integer', $state->{keyword})
1335 2788 50       8440 if $schema->{$state->{keyword}} < 0;
1336 2788         32546 return 1;
1337             }
1338              
1339 1928     1928 0 2358 sub assert_array_schemas ($schema, $state) {
  1928         2319  
  1928         2246  
  1928         2192  
1340 1928         3919 assert_keyword_type($state, $schema, 'array');
1341 1928 50       4824 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
1342 1928         2809 return 1;
1343             }
1344              
1345             # copied from JSON::Schema::Modern::Utilities::sprintf_num
1346 989     989 0 1350 sub sprintf_num ($value) {
  989         1286  
  989         1207  
1347             # use original value as stored in the NV, without losing precision
1348 989 100       1542 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
1349             }
1350              
1351             1;
1352              
1353             __END__