File Coverage

blib/lib/JSON/Schema/Tiny.pm
Criterion Covered Total %
statement 920 925 99.4
branch 512 564 90.7
condition 347 431 80.5
subroutine 103 103 100.0
pod 1 19 5.2
total 1883 2042 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   4782279 use strict;
  17         25  
  17         587  
3 17     17   64 use warnings;
  17         34  
  17         1031  
4 17     17   107 use if $ENV{AUTHOR_TESTING}, strictures => version => 2;
  17         64  
  17         961  
5             package JSON::Schema::Tiny; # git description: v0.033-12-g455ea01
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.034';
11              
12 17     17   219 use 5.020; # for unicode_strings, signatures, postderef features
  17         61  
13 17     17   86 use stable 0.031 'postderef';
  17         209  
  17         103  
14 17     17   4283 use experimental 0.026 qw(signatures args_array_with_signatures);
  17         212  
  17         72  
15 17     17   1491 use if $ENV{AUTHOR_TESTING}, autovivification => warn => qw(fetch store exists delete);
  17         25  
  17         870  
16 17     17   95 use if "$]" >= 5.022, experimental => 're_strict';
  17         31  
  17         281  
17 17     17   1035 no if "$]" >= 5.031009, feature => 'indirect';
  17         33  
  17         1051  
18 17     17   76 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         21  
  17         899  
19 17     17   75 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         24  
  17         741  
20 17     17   72 use B;
  17         54  
  17         405  
21 17     17   7964 use Mojo::URL;
  17         1888892  
  17         100  
22 17     17   7667 use Mojo::JSON::Pointer;
  17         12748  
  17         118  
23 17     17   781 use Carp qw(croak carp);
  17         54  
  17         886  
24 17     17   4546 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  17         238353  
  17         528  
25 17     17   4510 use Feature::Compat::Try;
  17         3139  
  17         128  
26 17     17   956 use JSON::PP ();
  17         71  
  17         560  
27 17     17   67 use if "$]" < 5.041010, 'List::Util' => 'any';
  17         29  
  17         618  
28 17     17   55 use if "$]" >= 5.041010, experimental => 'keyword_any';
  17         24  
  17         278  
29 17     17   978 use Scalar::Util 'looks_like_number';
  17         20  
  17         800  
30 17     17   6849 use builtin::compat qw(blessed created_as_number);
  17         179936  
  17         88  
31 17     17   2738 use if "$]" >= 5.022, POSIX => 'isinf';
  17         28  
  17         9071  
32 17     17   135994 use Math::BigFloat;
  17         1185242  
  17         85  
33 17     17   395922 use Exporter 5.57 'import';
  17         266  
  17         15983  
34              
35             our @EXPORT_OK = qw(evaluate);
36              
37             our $BOOLEAN_RESULT = 0;
38             our $SHORT_CIRCUIT = 0;
39             our $MAX_DEPTH = 50;
40             our $MAX_TRAVERSAL_DEPTH; # deprecated!
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 2811575 sub new ($class, %args) {
  18         30  
  18         66  
  18         51  
53 18         50 bless(\%args, $class);
54             }
55              
56             sub evaluate {
57 12777 50   12777 1 21379418 croak 'evaluate called in void context' if not defined wantarray;
58              
59 12777   66     42674 $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_DEPTH = $_[0]->{max_depth} // $_[0]->{max_traversal_depth} // $MAX_TRAVERSAL_DEPTH // $MAX_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     200932 local $SPECIFICATION_VERSION = $_[0]->{specification_version} // $SPECIFICATION_VERSION,
      66        
      66        
      66        
      33        
      33        
      33        
      33        
      66        
      100        
66             shift
67             if blessed($_[0]) and blessed($_[0])->isa(__PACKAGE__);
68              
69 12777 100       24424 if (defined $SPECIFICATION_VERSION) {
70             $SPECIFICATION_VERSION = 'draft'.$SPECIFICATION_VERSION
71 12631 100 100     36648 if $SPECIFICATION_VERSION !~ /^draft/ and any { 'draft'.$SPECIFICATION_VERSION eq $_ } values %version_uris;
  9         25  
72              
73 12631 100       25784 croak '$SPECIFICATION_VERSION value is invalid' if not any { $SPECIFICATION_VERSION eq $_ } values %version_uris;
  21785         49825  
74             }
75              
76 12776 50       21946 croak 'insufficient arguments' if @_ < 2;
77 12776         21131 my ($data, $schema) = @_;
78              
79 12776   100     39776 my $state = {
80             depth => 0,
81             data_path => '',
82             traversed_keyword_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             keyword_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             specification_version => $SPECIFICATION_VERSION,
90             };
91              
92 12776         178006 my $valid;
93 12776         18615 try {
94 12776         23023 $valid = _evaluate_subschema($data, $schema, $state)
95             }
96             catch ($e) {
97 1675 100       4133 if (ref $e eq 'HASH') {
98 1674         3001 push $state->{errors}->@*, $e;
99             }
100             else {
101 1         4 E($state, 'EXCEPTION: '.$e);
102             }
103              
104 1675         3536 $valid = 0;
105             }
106              
107 12776 50 66     33587 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       51085 $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   28241 sub _evaluate_subschema ($data, $schema, $state) {
  21636         26728  
  21636         23201  
  21636         22604  
  21636         22179  
138 21636 50       34234 croak '_evaluate_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         90967 $state = { %$state };
143 21636         110007 delete $state->@{'keyword', grep /^_/, keys %$state};
144              
145             abort($state, 'EXCEPTION: maximum evaluation depth exceeded')
146 21636 100       51602 if $state->{depth}++ > $MAX_DEPTH;
147              
148 21633         37539 my $schema_type = get_type($schema);
149 21633 100 66     39262 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
150 20882 100       31701 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
151              
152 20863 100       34326 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         32297 my $canonical_uri = canonical_uri($state);
157 20504         35908 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
158             {
159 17     17   5122 use autovivification qw(fetch store);
  17         8810  
  17         87  
  20504         21924  
160             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
161             if grep substr($schema_location, 0, length) eq $_,
162 20504 100       67194 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
163 20502         2774446 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
164             }
165              
166 20502         1863373 my $valid = 1;
167 20502   100     43415 my $specification_version = $state->{specification_version}//'';
168              
169 20502 100 100     529560 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             !$specification_version || $specification_version ne 'draft7' ? '$anchor' : (),
173             !$specification_version || $specification_version eq 'draft2019-09' ? '$recursiveAnchor' : (),
174             !$specification_version || $specification_version eq 'draft2020-12' ? '$dynamicAnchor' : (),
175             '$ref',
176             !$specification_version || $specification_version eq 'draft2019-09' ? '$recursiveRef' : (),
177             !$specification_version || $specification_version eq 'draft2020-12' ? '$dynamicRef' : (),
178             !$specification_version || $specification_version ne 'draft7' ? '$vocabulary' : (),
179             '$comment',
180             !$specification_version || $specification_version eq 'draft7' ? 'definitions' : (),
181             !$specification_version || $specification_version ne 'draft7' ? '$defs' : (),
182             # APPLICATOR KEYWORDS
183             qw(allOf anyOf oneOf not if),
184             !$specification_version || $specification_version ne 'draft7' ? 'dependentSchemas' : (),
185             !$specification_version || $specification_version eq 'draft7' ? 'dependencies' : (),
186             !$specification_version || $specification_version !~ qr/^draft(?:7|2019-09)\z/ ? 'prefixItems' : (),
187             'items',
188             !$specification_version || $specification_version =~ qr/^draft(?:7|2019-09)\z/ ? 'additionalItems' : (),
189             qw(contains properties patternProperties additionalProperties propertyNames),
190             # UNEVALUATED KEYWORDS
191             !$specification_version || $specification_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             !$specification_version || $specification_version ne 'draft7' ? qw(maxContains minContains) : (),
198             qw(maxProperties minProperties required),
199             !$specification_version || $specification_version ne 'draft7' ? 'dependentRequired' : (),
200             ) {
201 720298 100       997087 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     119309 and exists $schema->{'$ref'} and $specification_version eq 'draft7';
      100        
      100        
206              
207 34281         60263 $state->{keyword} = $keyword;
208 34281         45179 my $error_count = $state->{errors}->@*;
209              
210 34281         179289 my $sub = __PACKAGE__->can('_eval_keyword_'.($keyword =~ s/^\$//r));
211 34281 100       75901 if (not $sub->($data, $schema, $state)) {
212             warn 'result is false but there are no errors (keyword: '.$keyword.')'
213 7501 50       15038 if $error_count == $state->{errors}->@*;
214 7501         8952 $valid = 0;
215             }
216              
217 31682 100 100     162747 last if not $valid and $state->{short_circuit};
218             }
219              
220             # check for previously-supported but now removed keywords
221 17903   100     92015 foreach my $keyword (sort keys(($removed_keywords{$specification_version}//{})->%*)) {
222 62636 100       88460 next if not exists $schema->{$keyword};
223 214         385 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
224             .canonical_uri($state).'")';
225 214 50       17884 if (my $alternates = $removed_keywords{$specification_version}->{$keyword}) {
226 214         752 my @list = map '"'.$_.'"', @$alternates;
227 214 50       415 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
228 214 100       490 splice(@list, -1, 0, 'or') if @list > 1;
229 214         483 $message .= ': this should be rewritten as '.join(' ', @list);
230             }
231 214         22639 carp $message;
232             }
233              
234 17903         103151 return $valid;
235             }
236              
237             # KEYWORD IMPLEMENTATIONS
238              
239 5644     5644   5906 sub _eval_keyword_schema ($data, $schema, $state) {
  5644         6709  
  5644         6147  
  5644         5308  
  5644         5449  
240 5644         13253 assert_keyword_type($state, $schema, 'string');
241 5644         12623 assert_uri($state, $schema);
242              
243             return abort($state, '$schema can only appear at the schema resource root')
244 5644 100       11073 if length($state->{keyword_path});
245              
246 5643         11488 my $specification_version = $version_uris{$schema->{'$schema'}};
247 5643 100       9970 abort($state, 'custom $schema URIs are not supported (must be one of: %s',
248             join(', ', map '"'.$_.'"', keys %version_uris))
249             if not $specification_version;
250              
251 5614 100 100     14583 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 $specification_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     10931 if exists $schema->{'$ref'} and $specification_version eq 'draft7';
257              
258 5612         13819 $state->{specification_version} = $specification_version;
259             }
260              
261 1699     1699   1972 sub _eval_keyword_ref ($data, $schema, $state) {
  1699         2143  
  1699         1968  
  1699         1908  
  1699         1725  
262 1699         3561 assert_keyword_type($state, $schema, 'string');
263 1699         3730 assert_uri_reference($state, $schema);
264              
265 1699         5100 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     512169 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     16277 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
274              
275 1132   100     440284 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment//'');
276 1132 100       38268 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
277              
278             return _evaluate_subschema($data, $subschema,
279             +{ %$state,
280             traversed_keyword_path => $state->{traversed_keyword_path}.$state->{keyword_path}.'/'.$state->{keyword},
281 1127         10603 initial_schema_uri => $uri,
282             keyword_path => '',
283             });
284             }
285              
286 52     52   67 sub _eval_keyword_recursiveRef ($data, $schema, $state) {
  52         67  
  52         54  
  52         68  
  52         54  
287 52         110 assert_keyword_type($state, $schema, 'string');
288 52         110 assert_uri_reference($state, $schema);
289              
290 52         141 my $uri = Mojo::URL->new($schema->{'$recursiveRef'})->to_abs($state->{initial_schema_uri});
291 52 50 50     16743 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     412 if $uri->clone->fragment(undef) ne Mojo::URL->new($state->{root_schema}{'$id'}//'');
299              
300 8         3086 my $subschema = Mojo::JSON::Pointer->new($state->{root_schema})->get($uri->fragment);
301 8 50       159 abort($state, 'EXCEPTION: unable to find resource %s', $uri) if not defined $subschema;
302              
303 8 50 33     26 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 _evaluate_subschema($data, $subschema,
311             +{ %$state,
312 8         75 traversed_keyword_path => $state->{traversed_keyword_path}.$state->{keyword_path}.'/$recursiveRef',
313             initial_schema_uri => $uri,
314             keyword_path => '',
315             });
316             }
317              
318 12     12   28 sub _eval_keyword_dynamicRef { goto \&_eval_keyword_ref }
319              
320 670     670   799 sub _eval_keyword_id ($data, $schema, $state) {
  670         877  
  670         798  
  670         762  
  670         763  
321 670         1583 assert_keyword_type($state, $schema, 'string');
322 670         3680 assert_uri_reference($state, $schema);
323              
324 670         1652 my $uri = Mojo::URL->new($schema->{'$id'});
325              
326 670 100 100     52922 if (($state->{specification_version}//'') eq 'draft7') {
327 133 100       232 if (length($uri->fragment)) {
328 3 50       20 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       364 abort($state, '$id value does not match required syntax')
332             if $uri->fragment !~ m/^[A-Za-z][A-Za-z0-9_:.-]*\z/;
333              
334 2         20 return 1;
335             }
336             }
337             else {
338 537 100       988 abort($state, '$id value "%s" cannot have a non-empty fragment', $uri) if length $uri->fragment;
339             }
340              
341 665         2959 $uri->fragment(undef);
342 665 100       3274 return E($state, '$id cannot be empty') if not length $uri;
343              
344 641 100       76308 $state->{initial_schema_uri} = $uri->is_abs ? $uri : $uri->to_abs($state->{initial_schema_uri});
345 641         38789 $state->{traversed_keyword_path} = $state->{traversed_keyword_path}.$state->{keyword_path};
346 641         1036 $state->{keyword_path} = '';
347              
348 641         1595 return 1;
349             }
350              
351 12     12   14 sub _eval_keyword_anchor ($data, $schema, $state) {
  12         16  
  12         13  
  12         10  
  12         11  
352 12         26 assert_keyword_type($state, $schema, 'string');
353              
354             return 1 if
355             (!$state->{specification_version} or $state->{specification_version} eq 'draft2019-09')
356             and ($schema->{'$anchor'}//'') =~ /^[A-Za-z][A-Za-z0-9_:.-]*\z/
357             or
358             (!$state->{specification_version} or $state->{specification_version} eq 'draft2020-12')
359 12 50 66     114 and ($schema->{'$anchor'}//'') =~ /^[A-Za-z_][A-Za-z0-9._-]*\z/;
      50        
      66        
      33        
      50        
      33        
      66        
360              
361 0         0 abort($state, '$anchor value does not match required syntax');
362             }
363              
364 92     92   127 sub _eval_keyword_recursiveAnchor ($data, $schema, $state) {
  92         118  
  92         109  
  92         90  
  92         94  
365 92         184 assert_keyword_type($state, $schema, 'boolean');
366 92 100 100     275 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       351 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         99 $state->{recursive_anchor_uri} = canonical_uri($state);
376              
377 51         94 return 1;
378             }
379              
380 10     10   11 sub _eval_keyword_dynamicAnchor ($data, $schema, $state) {
  10         11  
  10         11  
  10         10  
  10         9  
381 10 50       17 return if not assert_keyword_type($state, $schema, 'string');
382              
383             abort($state, '$dynamicAnchor value does not match required syntax')
384 10 50       41 if $schema->{'$dynamicAnchor'} !~ /^[A-Za-z_][A-Za-z0-9._-]*\z/;
385 10         18 return 1;
386             }
387              
388 4     4   9 sub _eval_keyword_vocabulary ($data, $schema, $state) {
  4         5  
  4         6  
  4         4  
  4         6  
389 4         10 assert_keyword_type($state, $schema, 'object');
390              
391 4         11 foreach my $uri (sort keys $schema->{'$vocabulary'}->%*) {
392             abort({ %$state, _keyword_path_suffix => $uri }, '$vocabulary value at "%s" is not a boolean', $uri)
393 4 50       10 if not is_type('boolean', $schema->{'$vocabulary'}{$uri});
394              
395 4         9 assert_uri($state, undef, $uri);
396             }
397              
398             abort($state, '$vocabulary can only appear at the schema resource root')
399 4 50       10 if length($state->{keyword_path});
400              
401             abort($state, '$vocabulary can only appear at the document root')
402 4 50       10 if length($state->{traversed_keyword_path}.$state->{keyword_path});
403              
404 4         10 return 1;
405             }
406              
407 288     288   388 sub _eval_keyword_comment ($data, $schema, $state) {
  288         391  
  288         327  
  288         308  
  288         337  
408 288         653 assert_keyword_type($state, $schema, 'string');
409 288         567 return 1;
410             }
411              
412 150     150   449 sub _eval_keyword_definitions { goto \&_eval_keyword_defs }
413              
414 677     677   939 sub _eval_keyword_defs ($data, $schema, $state) {
  677         880  
  677         755  
  677         790  
  677         736  
415 677         1488 assert_keyword_type($state, $schema, 'object');
416 675         1377 return 1;
417             }
418              
419 3706     3706   4255 sub _eval_keyword_type ($data, $schema, $state) {
  3706         4515  
  3706         4255  
  3706         3947  
  3706         4028  
420 3706 100       6582 if (ref $schema->{type} eq 'ARRAY') {
421 163 50       352 abort($state, 'type array is empty') if not $schema->{type}->@*;
422 163         287 foreach my $type ($schema->{type}->@*) {
423             abort($state, 'unrecognized type "%s"', $type//'')
424 344 100 50     434 if not any { ($type//'') eq $_ } qw(null boolean object array string number integer);
  1397   100     2415  
425             }
426 157 50       355 abort($state, '"type" values are not unique') if not is_elements_unique($schema->{type});
427              
428 157         225 my $type = get_type($data);
429             return 1 if any {
430 292 100 100     1946 $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       328 } $schema->{type}->@*;
435 92         275 return E($state, 'got %s, not one of %s', $type, join(', ', $schema->{type}->@*));
436             }
437             else {
438 3543         7377 assert_keyword_type($state, $schema, 'string');
439             abort($state, 'unrecognized type "%s"', $schema->{type}//'')
440 3537 100 50     5850 if not any { ($schema->{type}//'') eq $_ } qw(null boolean object array string number integer);
  16514   50     30163  
441              
442 3535         5953 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     15922 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         1968 return E($state, 'got %s, not %s', $type, $schema->{type});
448             }
449             }
450              
451 483     483   594 sub _eval_keyword_enum ($data, $schema, $state) {
  483         656  
  483         575  
  483         544  
  483         511  
452 483         1115 assert_keyword_type($state, $schema, 'array');
453              
454 483         656 my @s; my $idx = 0;
  483         617  
455 483 100       882 return 1 if any { is_equal($data, $_, $s[$idx++] = {}) } $schema->{enum}->@*;
  933         10465  
456              
457             return E($state, 'value does not match'
458 219 100       1055 .(!(grep $_->{path}, @s) ? ''
459             : ' ('.join('; ', map "from enum $_ at '$s[$_]->{path}': $s[$_]->{error}", 0..$#s).')'));
460             }
461              
462 1055     1055   1213 sub _eval_keyword_const ($data, $schema, $state) {
  1055         1378  
  1055         1273  
  1055         1212  
  1055         1108  
463 1055 100       2390 return 1 if is_equal($data, $schema->{const}, my $s = {});
464 479 100       5856 return E($state, 'value does not match'.($s->{path} ? " (at '$s->{path}': $s->{error})" : ''));
465             }
466              
467 832     832   1063 sub _eval_keyword_multipleOf ($data, $schema, $state) {
  832         1114  
  832         1006  
  832         1011  
  832         858  
468 832         1944 assert_keyword_type($state, $schema, 'number');
469 830 50       1924 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     16059 and do { $data = 0+$data; 1 });
  2   66     13  
  2   33     8  
      100        
474              
475             # if either value is a float, use the bignum library for the calculation
476 640 100 100     940 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       87 $data = is_bignum($data) ? $data->copy : Math::BigFloat->new($data);
479 52 100       3092 my $divisor = is_bignum($schema->{multipleOf}) ? $schema->{multipleOf} : Math::BigFloat->new($schema->{multipleOf});
480 52         1466 my ($quotient, $remainder) = $data->bdiv($divisor);
481 52 50       53553 return E($state, 'overflow while calculating quotient') if $quotient->is_inf;
482 52 100       395 return 1 if $remainder == 0;
483             }
484             else {
485 588         1070 my $quotient = $data / $schema->{multipleOf};
486 588 50       3792 return E($state, 'overflow while calculating quotient')
    50          
487             if "$]" >= 5.022 ? isinf($quotient) : $quotient =~ /^-?Inf\z/i;
488 588 100       1756 return 1 if int($quotient) == $quotient;
489             }
490              
491 297         7136 return E($state, 'value is not a multiple of %s', sprintf_num($schema->{multipleOf}));
492             }
493              
494 585     585   831 sub _eval_keyword_maximum ($data, $schema, $state) {
  585         829  
  585         726  
  585         622  
  585         636  
495 585         1263 assert_keyword_type($state, $schema, 'number');
496 583 50 66     988 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       1114 return 1 if 0+$data <= $schema->{maximum};
499 172         8527 return E($state, 'value is larger than %s', sprintf_num($schema->{maximum}));
500             }
501              
502 483     483   663 sub _eval_keyword_exclusiveMaximum ($data, $schema, $state) {
  483         691  
  483         573  
  483         672  
  483         546  
503 483         1222 assert_keyword_type($state, $schema, 'number');
504 481 50 66     829 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       884 return 1 if 0+$data < $schema->{exclusiveMaximum};
507 154         12134 return E($state, 'value is equal to or larger than %s', sprintf_num($schema->{exclusiveMaximum}));
508             }
509              
510 713     713   947 sub _eval_keyword_minimum ($data, $schema, $state) {
  713         958  
  713         797  
  713         823  
  713         822  
511 713         1516 assert_keyword_type($state, $schema, 'number');
512 711 50 66     1128 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       1441 return 1 if 0+$data >= $schema->{minimum};
515 242         17544 return E($state, 'value is smaller than %s', sprintf_num($schema->{minimum}));
516             }
517              
518 423     423   598 sub _eval_keyword_exclusiveMinimum ($data, $schema, $state) {
  423         629  
  423         586  
  423         653  
  423         482  
519 423         1056 assert_keyword_type($state, $schema, 'number');
520 421 50 66     766 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       882 return 1 if 0+$data > $schema->{exclusiveMinimum};
523 124         9636 return E($state, 'value is equal to or smaller than %s', sprintf_num($schema->{exclusiveMinimum}));
524             }
525              
526 561     561   753 sub _eval_keyword_maxLength ($data, $schema, $state) {
  561         760  
  561         640  
  561         679  
  561         610  
527 561         1351 assert_non_negative_integer($schema, $state);
528              
529 561 100       946 return 1 if not is_type('string', $data);
530 352 100       991 return 1 if length($data) <= $schema->{maxLength};
531 162         1810 return E($state, 'length is greater than %d', $schema->{maxLength});
532             }
533              
534 512     512   725 sub _eval_keyword_minLength ($data, $schema, $state) {
  512         670  
  512         641  
  512         600  
  512         542  
535 512         1206 assert_non_negative_integer($schema, $state);
536              
537 512 100       886 return 1 if not is_type('string', $data);
538 302 100       845 return 1 if length($data) >= $schema->{minLength};
539 142         1781 return E($state, 'length is less than %d', $schema->{minLength});
540             }
541              
542 899     899   1036 sub _eval_keyword_pattern ($data, $schema, $state) {
  899         1180  
  899         1003  
  899         991  
  899         924  
543 899         1802 assert_keyword_type($state, $schema, 'string');
544 899         2129 assert_pattern($state, $schema->{pattern});
545              
546 898 100       1370 return 1 if not is_type('string', $data);
547 671 100       4458 return 1 if $data =~ m/(?:$schema->{pattern})/;
548 313         555 return E($state, 'pattern does not match');
549             }
550              
551 425     425   560 sub _eval_keyword_maxItems ($data, $schema, $state) {
  425         536  
  425         463  
  425         467  
  425         426  
552 425         942 assert_non_negative_integer($schema, $state);
553              
554 425 100       631 return 1 if not is_type('array', $data);
555 256 100       652 return 1 if @$data <= $schema->{maxItems};
556 122 100       1800 return E($state, 'more than %d item%s', $schema->{maxItems}, $schema->{maxItems} > 1 ? 's' : '');
557             }
558              
559 424     424   518 sub _eval_keyword_minItems ($data, $schema, $state) {
  424         500  
  424         456  
  424         491  
  424         424  
560 424         992 assert_non_negative_integer($schema, $state);
561              
562 424 100       601 return 1 if not is_type('array', $data);
563 257 100       679 return 1 if @$data >= $schema->{minItems};
564 124 100       1599 return E($state, 'fewer than %d item%s', $schema->{minItems}, $schema->{minItems} > 1 ? 's' : '');
565             }
566              
567 775     775   988 sub _eval_keyword_uniqueItems ($data, $schema, $state) {
  775         953  
  775         875  
  775         904  
  775         796  
568 775         1611 assert_keyword_type($state, $schema, 'boolean');
569 775 100       1306 return 1 if not is_type('array', $data);
570 614 100       2078 return 1 if not $schema->{uniqueItems};
571 449 100       3391 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   98 sub _eval_keyword_maxContains ($data, $schema, $state) {
  84         93  
  84         89  
  84         88  
  84         82  
576 84         203 assert_non_negative_integer($schema, $state);
577 84 100       171 return 1 if not exists $state->{_num_contains};
578 76 50       140 return 1 if not is_type('array', $data);
579              
580             return E($state, 'contains too many matching items')
581 76 100       171 if $state->{_num_contains} > $schema->{maxContains};
582              
583 44         1112 return 1;
584             }
585              
586 102     102   144 sub _eval_keyword_minContains ($data, $schema, $state) {
  102         120  
  102         99  
  102         131  
  102         103  
587 102         243 assert_non_negative_integer($schema, $state);
588 102 100       183 return 1 if not exists $state->{_num_contains};
589 94 50       140 return 1 if not is_type('array', $data);
590              
591             return E($state, 'contains too few matching items')
592 94 100       217 if $state->{_num_contains} < $schema->{minContains};
593              
594 60         1198 return 1;
595             }
596              
597 340     340   424 sub _eval_keyword_maxProperties ($data, $schema, $state) {
  340         413  
  340         404  
  340         383  
  340         356  
598 340         764 assert_non_negative_integer($schema, $state);
599              
600 340 100       514 return 1 if not is_type('object', $data);
601 202 100       578 return 1 if keys %$data <= $schema->{maxProperties};
602             return E($state, 'more than %d propert%s', $schema->{maxProperties},
603 98 100       1875 $schema->{maxProperties} > 1 ? 'ies' : 'y');
604             }
605              
606 340     340   444 sub _eval_keyword_minProperties ($data, $schema, $state) {
  340         437  
  340         410  
  340         395  
  340         352  
607 340         754 assert_non_negative_integer($schema, $state);
608              
609 340 100       511 return 1 if not is_type('object', $data);
610 202 100       566 return 1 if keys %$data >= $schema->{minProperties};
611             return E($state, 'fewer than %d propert%s', $schema->{minProperties},
612 98 100       1632 $schema->{minProperties} > 1 ? 'ies' : 'y');
613             }
614              
615 1414     1414   1621 sub _eval_keyword_required ($data, $schema, $state) {
  1414         1795  
  1414         1583  
  1414         1477  
  1414         1455  
616 1414         2547 assert_keyword_type($state, $schema, 'array');
617             abort($state, '"required" element is not a string')
618 1414 50       2546 if any { !is_type('string', $_) } $schema->{required}->@*;
  1600         2359  
619 1414 50       2714 abort($state, '"required" values are not unique') if not is_elements_unique($schema->{required});
620              
621 1414 100       2079 return 1 if not is_type('object', $data);
622              
623 1264         3252 my @missing = grep !exists $data->{$_}, $schema->{required}->@*;
624 1264 100       2749 return 1 if not @missing;
625 566 100       1976 return E($state, 'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
626             }
627              
628 271     271   393 sub _eval_keyword_dependentRequired ($data, $schema, $state) {
  271         333  
  271         312  
  271         299  
  271         283  
629 271         570 assert_keyword_type($state, $schema, 'object');
630              
631 271         773 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
632             E({ %$state, _keyword_path_suffix => $property }, 'value is not an array'), next
633 287 50       519 if not is_type('array', $schema->{dependentRequired}{$property});
634              
635 287         701 foreach my $index (0..$schema->{dependentRequired}{$property}->$#*) {
636             abort({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
637 301 100       511 if not is_type('string', $schema->{dependentRequired}{$property}[$index]);
638             }
639              
640             abort({ %$state, _keyword_path_suffix => $property }, 'elements are not unique')
641 286 50       609 if not is_elements_unique($schema->{dependentRequired}{$property});
642             }
643              
644 270 100       426 return 1 if not is_type('object', $data);
645              
646 173         224 my $valid = 1;
647 173         377 foreach my $property (sort keys $schema->{dependentRequired}->%*) {
648 189 100       352 next if not exists $data->{$property};
649              
650 153 100       607 if (my @missing = grep !exists($data->{$_}), $schema->{dependentRequired}{$property}->@*) {
651 79 100       702 $valid = E({ %$state, _keyword_path_suffix => $property },
652             'missing propert%s: %s', @missing > 1 ? 'ies' : 'y', join(', ', @missing));
653             }
654             }
655              
656 173 100       389 return 1 if $valid;
657 79         135 return E($state, 'not all dependencies are satisfied');
658             }
659              
660 575     575   798 sub _eval_keyword_allOf ($data, $schema, $state) {
  575         754  
  575         703  
  575         659  
  575         682  
661 575         1339 assert_array_schemas($schema, $state);
662              
663 575         698 my @invalid;
664 575         1392 foreach my $idx (0..$schema->{allOf}->$#*) {
665             next if _evaluate_subschema($data, $schema->{allOf}[$idx],
666 843 100       6728 +{ %$state, keyword_path => $state->{keyword_path}.'/allOf/'.$idx });
667              
668 209         747 push @invalid, $idx;
669 209 100       468 last if $state->{short_circuit};
670             }
671              
672 404 100       1237 return 1 if @invalid == 0;
673              
674 169         261 my $pl = @invalid > 1;
675 169 100       606 return E($state, 'subschema%s %s %s not valid', $pl?'s':'', join(', ', @invalid), $pl?'are':'is');
    100          
676             }
677              
678 433     433   561 sub _eval_keyword_anyOf ($data, $schema, $state) {
  433         574  
  433         514  
  433         467  
  433         432  
679 433         998 assert_array_schemas($schema, $state);
680              
681 433         495 my $valid = 0;
682 433         550 my @errors;
683 433         1055 foreach my $idx (0..$schema->{anyOf}->$#*) {
684             next if not _evaluate_subschema($data, $schema->{anyOf}[$idx],
685 760 100       6191 +{ %$state, errors => \@errors, keyword_path => $state->{keyword_path}.'/anyOf/'.$idx });
686 239         1079 ++$valid;
687 239 100       527 last if $state->{short_circuit};
688             }
689              
690 294 100       781 return 1 if $valid;
691 92         197 push $state->{errors}->@*, @errors;
692 92         156 return E($state, 'no subschemas are valid');
693             }
694              
695 509     509   537 sub _eval_keyword_oneOf ($data, $schema, $state) {
  509         601  
  509         526  
  509         543  
  509         502  
696 509         1092 assert_array_schemas($schema, $state);
697              
698 509         603 my (@valid, @errors);
699 509         1282 foreach my $idx (0..$schema->{oneOf}->$#*) {
700             next if not _evaluate_subschema($data, $schema->{oneOf}[$idx],
701 1061 100       8275 +{ %$state, errors => \@errors, keyword_path => $state->{keyword_path}.'/oneOf/'.$idx });
702 377         1554 push @valid, $idx;
703 377 100 100     1095 last if @valid > 1 and $state->{short_circuit};
704             }
705              
706 358 100       1076 return 1 if @valid == 1;
707              
708 201 100       335 if (not @valid) {
709 123         254 push $state->{errors}->@*, @errors;
710 123         233 return E($state, 'no subschemas are valid');
711             }
712             else {
713 78         267 return E($state, 'multiple subschemas are valid: '.join(', ', @valid));
714             }
715             }
716              
717 293     293   391 sub _eval_keyword_not ($data, $schema, $state) {
  293         371  
  293         338  
  293         319  
  293         317  
718 293 100 66     625 return !$schema->{not} || E($state, 'subschema is true') if is_type('boolean', $schema->{not});
719              
720             return 1 if not _evaluate_subschema($data, $schema->{not},
721 181 100       1552 +{ %$state, keyword_path => $state->{keyword_path}.'/not', short_circuit => 1, errors => [] });
722              
723 135         539 return E($state, 'subschema is valid');
724             }
725              
726 326     326   424 sub _eval_keyword_if ($data, $schema, $state) {
  326         431  
  326         378  
  326         328  
  326         324  
727 326 100 100     826 return 1 if not exists $schema->{then} and not exists $schema->{else};
728             my $keyword = _evaluate_subschema($data, $schema->{if},
729 282 100       2298 +{ %$state, keyword_path => $state->{keyword_path}.'/if', short_circuit => 1, errors => [] })
730             ? 'then' : 'else';
731              
732 282 100       1402 return 1 if not exists $schema->{$keyword};
733              
734             return $schema->{$keyword} || E({ %$state, keyword => $keyword }, 'subschema is false')
735 224 100 66     441 if is_type('boolean', $schema->{$keyword});
736              
737             return 1 if _evaluate_subschema($data, $schema->{$keyword},
738 192 100       1244 +{ %$state, keyword_path => $state->{keyword_path}.'/'.$keyword });
739 62         468 return E({ %$state, keyword => $keyword }, 'subschema is not valid');
740             }
741              
742 337     337   476 sub _eval_keyword_dependentSchemas ($data, $schema, $state) {
  337         454  
  337         408  
  337         367  
  337         328  
743 337         754 assert_keyword_type($state, $schema, 'object');
744              
745 337 100       532 return 1 if not is_type('object', $data);
746              
747 213         291 my $valid = 1;
748 213         635 foreach my $property (sort keys $schema->{dependentSchemas}->%*) {
749             next if not exists $data->{$property}
750             or _evaluate_subschema($data, $schema->{dependentSchemas}{$property},
751 263 100 100     972 +{ %$state, keyword_path => jsonp($state->{keyword_path}, 'dependentSchemas', $property) });
752              
753 97         326 $valid = 0;
754 97 100       234 last if $state->{short_circuit};
755             }
756              
757 213 100       601 return E($state, 'not all dependencies are satisfied') if not $valid;
758 116         223 return 1;
759             }
760              
761 186     186   236 sub _eval_keyword_dependencies ($data, $schema, $state) {
  186         245  
  186         219  
  186         219  
  186         200  
762 186         443 assert_keyword_type($state, $schema, 'object');
763              
764 186 100       281 return 1 if not is_type('object', $data);
765              
766 119         167 my $valid = 1;
767 119         318 foreach my $property (sort keys $schema->{dependencies}->%*) {
768 166 100       352 if (is_type('array', $schema->{dependencies}{$property})) {
769             # as in dependentRequired
770              
771 52         113 foreach my $index (0..$schema->{dependencies}{$property}->$#*) {
772             $valid = E({ %$state, _keyword_path_suffix => [ $property, $index ] }, 'element #%d is not a string', $index)
773 62 50       99 if not is_type('string', $schema->{dependencies}{$property}[$index]);
774             }
775              
776             abort({ %$state, _keyword_path_suffix => $property }, 'elements are not unique')
777 52 50       98 if not is_elements_unique($schema->{dependencies}{$property});
778              
779 52 100       103 next if not exists $data->{$property};
780              
781 24 100       86 if (my @missing = grep !exists($data->{$_}), $schema->{dependencies}{$property}->@*) {
782 14 100       116 $valid = E({ %$state, _keyword_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 _evaluate_subschema($data, $schema->{dependencies}{$property},
790 114 100 100     437 +{ %$state, keyword_path => jsonp($state->{keyword_path}, 'dependencies', $property) });
791              
792 47         156 $valid = 0;
793 47 100       113 last if $state->{short_circuit};
794             }
795             }
796              
797 119 100       336 return 1 if $valid;
798 59         108 return E($state, 'not all dependencies are satisfied');
799             }
800              
801 411     411   514 sub _eval_keyword_prefixItems ($data, $schema, $state) {
  411         477  
  411         432  
  411         466  
  411         437  
802 411 50       775 return if not assert_array_schemas($schema, $state);
803 411         1151 goto \&_eval_keyword__items_array_schemas;
804             }
805              
806 1304     1304   1653 sub _eval_keyword_items ($data, $schema, $state) {
  1304         1598  
  1304         1390  
  1304         1585  
  1304         1327  
807 1304 100       3025 if (ref $schema->{items} eq 'ARRAY') {
808             abort($state, 'array form of "items" not supported in %s', $state->{specification_version})
809 700 100 100     1635 if ($state->{specification_version}//'') eq 'draft2020-12';
810              
811 699         2080 goto \&_eval_keyword__items_array_schemas;
812             }
813              
814 604   100     2190 $state->{_last_items_index} //= -1;
815 604         1716 goto \&_eval_keyword__items_schema;
816             }
817              
818 219     219   227 sub _eval_keyword_additionalItems ($data, $schema, $state) {
  219         253  
  219         240  
  219         217  
  219         230  
819 219 100       413 return 1 if not exists $state->{_last_items_index};
820 191         476 goto \&_eval_keyword__items_schema;
821             }
822              
823             # prefixItems (draft 2020-12), array-based items (all drafts)
824 1110     1110   1346 sub _eval_keyword__items_array_schemas ($data, $schema, $state) {
  1110         1328  
  1110         1171  
  1110         1079  
  1110         1175  
825 1110 50       2192 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
826 1110 100       1860 return 1 if not is_type('array', $data);
827              
828 897         1177 my $valid = 1;
829              
830 897         2106 foreach my $idx (0..$data->$#*) {
831 1581 100       4233 last if $idx > $schema->{$state->{keyword}}->$#*;
832 1294         2402 $state->{_last_items_index} = $idx;
833              
834 1294 100       2088 if (is_type('boolean', $schema->{$state->{keyword}}[$idx])) {
835 286 100       942 next if $schema->{$state->{keyword}}[$idx];
836 108         1553 $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx,
837             _keyword_path_suffix => $idx }, 'item not permitted');
838             }
839             else {
840             next if _evaluate_subschema($data->[$idx], $schema->{$state->{keyword}}[$idx],
841             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
842 1008 100       8650 keyword_path => $state->{keyword_path}.'/'.$state->{keyword}.'/'.$idx });
843             }
844              
845 175         615 $valid = 0;
846             last if $state->{short_circuit} and not exists $schema->{
847             $state->{keyword} eq 'prefixItems' ? 'items'
848 175 50 100     826 : $state->{keyword} eq 'items' ? 'additionalItems' : die
    100          
    100          
849             };
850             }
851              
852 897 100       1752 return E($state, 'not all items are valid') if not $valid;
853 725         1305 return 1;
854             }
855              
856             # schema-based items (all drafts), and additionalItems (drafts 4,6,7,2019-09)
857 795     795   936 sub _eval_keyword__items_schema ($data, $schema, $state) {
  795         955  
  795         837  
  795         760  
  795         736  
858 795 100       1265 return 1 if not is_type('array', $data);
859 691 100       1602 return 1 if $state->{_last_items_index} == $data->$#*;
860              
861 447         587 my $valid = 1;
862 447         1091 foreach my $idx ($state->{_last_items_index}+1 .. $data->$#*) {
863 676 100 100     1485 if (is_type('boolean', $schema->{$state->{keyword}})
864             and ($state->{keyword} eq 'additionalItems')) {
865 32 100       115 next if $schema->{$state->{keyword}};
866             $valid = E({ %$state, data_path => $state->{data_path}.'/'.$idx },
867             '%sitem not permitted',
868 26 50 33     430 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '');
869             }
870             else {
871             next if _evaluate_subschema($data->[$idx], $schema->{$state->{keyword}},
872             +{ %$state, data_path => $state->{data_path}.'/'.$idx,
873 644 100       5471 keyword_path => $state->{keyword_path}.'/'.$state->{keyword} });
874 219         734 $valid = 0;
875             }
876              
877 245 100       665 last if $state->{short_circuit};
878             }
879              
880 382         805 $state->{_last_items_index} = $data->$#*;
881              
882             return E($state, 'subschema is not valid against all %sitems',
883 382 100 100     1211 exists $schema->{prefixItems} || $state->{keyword} eq 'additionalItems' ? 'additional ' : '')
    100          
884             if not $valid;
885 179         302 return 1;
886             }
887              
888 717     717   943 sub _eval_keyword_contains ($data, $schema, $state) {
  717         965  
  717         902  
  717         801  
  717         797  
889 717 100       1238 return 1 if not is_type('array', $data);
890              
891 504         1143 $state->{_num_contains} = 0;
892 504         663 my @errors;
893 504         1254 foreach my $idx (0..$data->$#*) {
894 622 100       6699 if (_evaluate_subschema($data->[$idx], $schema->{contains},
895             +{ %$state, errors => \@errors,
896             data_path => $state->{data_path}.'/'.$idx,
897             keyword_path => $state->{keyword_path}.'/contains' })) {
898 390         2108 ++$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     2415 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     4352 if (not $state->{_num_contains} and (($schema->{minContains}//1) > 0
      66        
908             or $state->{specification_version} and $state->{specification_version} eq 'draft7')) {
909 195         338 push $state->{errors}->@*, @errors;
910 195         367 return E($state, 'subschema is not valid against any item');
911             }
912              
913 309         754 return 1;
914             }
915              
916 2401     2401   2786 sub _eval_keyword_properties ($data, $schema, $state) {
  2401         3048  
  2401         2751  
  2401         2626  
  2401         2384  
917 2401         5101 assert_keyword_type($state, $schema, 'object');
918 2401 100       3799 return 1 if not is_type('object', $data);
919              
920 2150         2966 my $valid = 1;
921 2150         5559 foreach my $property (sort keys $schema->{properties}->%*) {
922 2714 100       5114 next if not exists $data->{$property};
923              
924 1670 100       2887 if (is_type('boolean', $schema->{properties}{$property})) {
925 323 100       1073 next if $schema->{properties}{$property};
926 106         1050 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
927             _keyword_path_suffix => $property }, 'property not permitted');
928             }
929             else {
930             next if _evaluate_subschema($data->{$property}, $schema->{properties}{$property},
931             +{ %$state,
932             data_path => jsonp($state->{data_path}, $property),
933 1347 100       5192 keyword_path => jsonp($state->{keyword_path}, 'properties', $property) });
934              
935 329         1076 $valid = 0;
936             }
937 435 100       1337 last if $state->{short_circuit};
938             }
939              
940 1995 100       4874 return E($state, 'not all properties are valid') if not $valid;
941 1582         2819 return 1;
942             }
943              
944 809     809   1026 sub _eval_keyword_patternProperties ($data, $schema, $state) {
  809         988  
  809         834  
  809         910  
  809         891  
945 809         1666 assert_keyword_type($state, $schema, 'object');
946              
947 809         2443 foreach my $property (sort keys $schema->{patternProperties}->%*) {
948 1250         7073 assert_pattern({ %$state, _keyword_path_suffix => $property }, $property);
949             }
950              
951 807 100       1518 return 1 if not is_type('object', $data);
952              
953 614         825 my $valid = 1;
954 614         1349 foreach my $property_pattern (sort keys $schema->{patternProperties}->%*) {
955 898         8945 foreach my $property (sort grep m/(?:$property_pattern)/, keys %$data) {
956 557 100       1244 if (is_type('boolean', $schema->{patternProperties}{$property_pattern})) {
957 319 100       1184 next if $schema->{patternProperties}{$property_pattern};
958 108         975 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property),
959             _keyword_path_suffix => $property_pattern }, 'property not permitted');
960             }
961             else {
962             next if _evaluate_subschema($data->{$property}, $schema->{patternProperties}{$property_pattern},
963             +{ %$state,
964             data_path => jsonp($state->{data_path}, $property),
965 238 100       819 keyword_path => jsonp($state->{keyword_path}, 'patternProperties', $property_pattern) });
966              
967 87         285 $valid = 0;
968             }
969 195 100       855 last if $state->{short_circuit};
970             }
971             }
972              
973 614 100       2576 return E($state, 'not all properties are valid') if not $valid;
974 434         805 return 1;
975             }
976              
977 755     755   1000 sub _eval_keyword_additionalProperties ($data, $schema, $state) {
  755         912  
  755         910  
  755         832  
  755         767  
978 755 100       1334 return 1 if not is_type('object', $data);
979              
980 556         754 my $valid = 1;
981 556         1237 foreach my $property (sort keys %$data) {
982 552 100 100     1541 next if exists $schema->{properties} and exists $schema->{properties}{$property};
983             next if exists $schema->{patternProperties}
984 438 100 100     1031 and any { $property =~ /(?:$_)/ } keys $schema->{patternProperties}->%*;
  148         1266  
985              
986 350 100       622 if (is_type('boolean', $schema->{additionalProperties})) {
987 192 100       672 next if $schema->{additionalProperties};
988              
989 172         1563 $valid = E({ %$state, data_path => jsonp($state->{data_path}, $property) },
990             'additional property not permitted');
991             }
992             else {
993             next if _evaluate_subschema($data->{$property}, $schema->{additionalProperties},
994             +{ %$state,
995             data_path => jsonp($state->{data_path}, $property),
996 158 100       611 keyword_path => $state->{keyword_path}.'/additionalProperties' });
997              
998 43         149 $valid = 0;
999             }
1000 215 100       1004 last if $state->{short_circuit};
1001             }
1002              
1003 504 100       1124 return E($state, 'not all additional properties are valid') if not $valid;
1004 290         508 return 1;
1005             }
1006              
1007 463     463   644 sub _eval_keyword_propertyNames ($data, $schema, $state) {
  463         677  
  463         585  
  463         538  
  463         528  
1008 463 100       897 return 1 if not is_type('object', $data);
1009              
1010 288         390 my $valid = 1;
1011 288         739 foreach my $property (sort keys %$data) {
1012             next if _evaluate_subschema($property, $schema->{propertyNames},
1013             +{ %$state,
1014             data_path => jsonp($state->{data_path}, $property),
1015 202 100       823 keyword_path => $state->{keyword_path}.'/propertyNames' });
1016              
1017 116         437 $valid = 0;
1018 116 100       299 last if $state->{short_circuit};
1019             }
1020              
1021 288 100       692 return E($state, 'not all property names are valid') if not $valid;
1022 172         348 return 1;
1023             }
1024              
1025 384     384   495 sub _eval_keyword_unevaluatedItems ($data, $schema, $state) {
  384         573  
  384         461  
  384         483  
  384         411  
1026 384         828 abort($state, 'keyword not yet supported');
1027             }
1028              
1029 584     584   833 sub _eval_keyword_unevaluatedProperties ($data, $schema, $state) {
  584         726  
  584         709  
  584         724  
  584         593  
1030 584         1130 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 892669 sub is_type ($type, $value) {
  52894         56514  
  52894         57061  
  52894         49657  
1041 52894 100       76895 if ($type eq 'null') {
1042 83         274 return !(defined $value);
1043             }
1044 52811 100       69516 if ($type eq 'boolean') {
1045 6022         8990 return is_bool($value);
1046             }
1047 46789 100       64379 if ($type eq 'object') {
1048 12088         30203 return ref $value eq 'HASH';
1049             }
1050 34701 100       50930 if ($type eq 'array') {
1051 8790         23477 return ref $value eq 'ARRAY';
1052             }
1053              
1054 25911 100 100     57889 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
1055 25895 100       39269 return 0 if not defined $value;
1056 25877         84526 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       47172 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   159896 || do { no warnings 'numeric'; 0+$value eq $value });
  17         34  
  17         6842  
  16845         88236  
1069             }
1070              
1071 9032 100       14914 if ($type eq 'number') {
1072             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
1073 6143   100     9590 return is_bignum($value) || created_as_number($value);
1074             }
1075              
1076 2889 50       4587 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     4751 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       71 if ($type =~ /^reference to (.+)\z/) {
1086 11   33     66 return !blessed($value) && ref($value) eq $1;
1087             }
1088              
1089 5         23 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 760316 sub get_type ($value) {
  35088         37824  
  35088         33421  
1096 35088 100       73406 return 'object' if ref $value eq 'HASH';
1097 13041 100       17259 return 'boolean' if is_bool($value);
1098 11062 100       21890 return 'null' if not defined $value;
1099 10764 100       16121 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       14373 if (length(my $ref = ref $value)) {
1103 455 100       1704 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         21779 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   100 || do { no warnings 'numeric'; 0+$value eq $value });
  17   100     27  
  17         2257  
  9026         23746  
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       14928 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         47 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   107 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  17         86  
  17         1476  
1135 17     17   148 use if HAVE_BUILTIN, experimental => 'builtin';
  17         40  
  17         510  
1136 19063     19063 0 18223 sub is_bool ($value) {
  19063         19785  
  19063         18377  
1137 19063 50 66     71061 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 11523 sub is_bignum ($value) {
  11375         12628  
  11375         11232  
1146 11375         55566 ref($value) =~ /^Math::Big(?:Int|Float)\z/;
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 4691 sub is_equal ($x, $y, $state = {}) {
  4171         4529  
  4171         4525  
  4171         4770  
  4171         4184  
1158 4171   100     12692 $state->{path} //= '';
1159              
1160 4171         7174 my @types = map get_type($_), $x, $y;
1161              
1162 4171 100       10273 $state->{error} = 'ambiguous type encountered', return 0
1163             if grep $types[$_] eq 'ambiguous type', 0..1;
1164              
1165 4168 100       6829 if ($SCALARREF_BOOLEANS) {
1166 99 100       154 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
1167 99 100       145 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
1168             }
1169              
1170 4168 100       5915 if ($STRINGY_NUMBERS) {
1171 18 100 100     103 ($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     94 ($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       8506 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
1179 3325 100       4810 return 1 if $types[0] eq 'null';
1180 3311 100 100     9212 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
1181             if $types[0] eq 'string';
1182 1778 100 100     6800 ($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         884 my $path = $state->{path};
1186 623 100       1016 if ($types[0] eq 'object') {
1187 217 100       519 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
1188             if keys %$x != keys %$y;
1189              
1190 200 100       883 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         20 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
1193 7         32 return 0;
1194             }
1195              
1196 193         449 foreach my $property (sort keys %$x) {
1197 231         379 $state->{path} = jsonp($path, $property);
1198 231 100       410 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
1199             }
1200              
1201 106         568 return 1;
1202             }
1203              
1204 406 50       668 if ($types[0] eq 'array') {
1205 406 100       780 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
1206 397         870 foreach my $idx (0 .. $x->$#*) {
1207 441         971 $state->{path} = $path.'/'.$idx;
1208 441 100       1070 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
1209             }
1210 269         6024 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 2813 sub is_elements_unique ($array, $equal_indices = undef) {
  2358         2680  
  2358         2841  
  2358         2439  
1223 2358         5484 foreach my $idx0 (0 .. $array->$#*-1) {
1224 846         1598 foreach my $idx1 ($idx0+1 .. $array->$#*) {
1225 1251 100       2342 if (is_equal($array->[$idx0], $array->[$idx1])) {
1226 207 50       2307 push @$equal_indices, $idx0, $idx1 if defined $equal_indices;
1227 207         565 return 0;
1228             }
1229             }
1230             }
1231 2151         4818 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 142368 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 34349 sub canonical_uri ($state, @extra_path) {
  30612         33228  
  30612         34763  
  30612         33013  
1245 30612 100 100     82278 return $state->{initial_schema_uri} if not @extra_path and not length($state->{keyword_path});
1246 16601         41395 my $uri = $state->{initial_schema_uri}->clone;
1247 16601 100 100     993485 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{keyword_path}, @extra_path) : $state->{keyword_path});
1248 16601 100       55837 undef $fragment if not length($fragment);
1249 16601         31331 $uri->fragment($fragment);
1250 16601         75694 $uri;
1251             }
1252              
1253             # shorthand for creating error objects
1254             # based on JSON::Schema::Modern::Utilities::E
1255 9843     9843 0 29105 sub E ($state, $error_string, @args) {
  9843         11243  
  9843         11240  
  9843         12324  
  9843         9953  
1256             # sometimes the keyword shouldn't be at the very end of the schema path
1257 9843         15671 my $sps = delete $state->{_keyword_path_suffix};
1258 9843 100 100     33576 my @keyword_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
1259              
1260 9843         17765 my $uri = canonical_uri($state, $state->{keyword}, @keyword_path_suffix);
1261              
1262             my $keyword_location = $state->{traversed_keyword_path}
1263 9843         19837 .jsonp($state->{keyword_path}, $state->{keyword}, @keyword_path_suffix);
1264              
1265 9843 100 100     21936 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       2476583 keywordLocation => $keyword_location,
    100          
1271             defined $uri ? ( absoluteKeywordLocation => $uri->to_string) : (),
1272             error => @args ? sprintf($error_string, @args) : $error_string,
1273             };
1274              
1275 9843         199773 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 208717 sub abort ($state, $error_string, @args) {
  1674         2109  
  1674         2152  
  1674         2109  
  1674         1813  
1282 1674         3675 E($state, $error_string, @args);
1283 1674         25699 die pop $state->{errors}->@*;
1284             }
1285              
1286             # one common usecase of abort()
1287 28018     28018 0 29420 sub assert_keyword_type ($state, $schema, $type) {
  28018         30117  
  28018         29776  
  28018         31821  
  28018         27829  
1288 28018 100       55417 return 1 if is_type($type, $schema->{$state->{keyword}});
1289 18 100       84 abort($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
1290             }
1291              
1292 2149     2149 0 2317 sub assert_pattern ($state, $pattern) {
  2149         2269  
  2149         2311  
  2149         2266  
1293 2149         2902 try {
1294 2149     1   11381 local $SIG{__WARN__} = sub { die @_ };
  1         35  
1295 2149         23573 qr/$pattern/;
1296             }
1297 3         9 catch ($e) { abort($state, $e); }
1298 2146         6142 return 1;
1299             }
1300              
1301             # based on JSON::Schema::Modern::Utilities::assert_uri_reference
1302 2421     2421 0 2661 sub assert_uri_reference ($state, $schema) {
  2421         2524  
  2421         2378  
  2421         2467  
1303 2421         3615 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     5396 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         636694 return 1;
1314             }
1315              
1316             # based on JSON::Schema::Modern::Utilities::assert_uri
1317 5648     5648 0 5665 sub assert_uri ($state, $schema, $override = undef) {
  5648         5743  
  5648         5718  
  5648         6565  
  5648         5606  
1318 5648   66     13438 my $string = $override // $schema->{$state->{keyword}};
1319 5648         12166 my $uri = Mojo::URL->new($string);
1320              
1321 5648 0 33     353168 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         848056 return 1;
1332             }
1333              
1334 2788     2788 0 3304 sub assert_non_negative_integer ($schema, $state) {
  2788         3087  
  2788         3191  
  2788         2994  
1335 2788         5536 assert_keyword_type($state, $schema, 'integer');
1336             abort($state, '%s value is not a non-negative integer', $state->{keyword})
1337 2788 50       7427 if $schema->{$state->{keyword}} < 0;
1338 2788         31466 return 1;
1339             }
1340              
1341 1928     1928 0 2302 sub assert_array_schemas ($schema, $state) {
  1928         2149  
  1928         2104  
  1928         1892  
1342 1928         3753 assert_keyword_type($state, $schema, 'array');
1343 1928 50       3941 abort($state, '%s array is empty', $state->{keyword}) if not $schema->{$state->{keyword}}->@*;
1344 1928         2702 return 1;
1345             }
1346              
1347             # copied from JSON::Schema::Modern::Utilities::sprintf_num
1348 989     989 0 1240 sub sprintf_num ($value) {
  989         1129  
  989         975  
1349             # use original value as stored in the NV, without losing precision
1350 989 100       1457 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
1351             }
1352              
1353             {
1354             # avoid dependency on namespace::clean
1355 17     17   40314 no strict 'refs';
  17         47  
  17         1549  
1356             delete @{__PACKAGE__.'::'}{qw(croak carp any looks_like_number isinf)};
1357             }
1358              
1359             1;
1360              
1361             __END__