File Coverage

blib/lib/JSON/Schema/Modern/Utilities.pm
Criterion Covered Total %
statement 348 359 96.9
branch 160 182 87.9
condition 120 141 85.1
subroutine 59 62 95.1
pod 14 29 48.2
total 701 773 90.6


line stmt bran cond sub pod time code
1 45     45   337 use strict;
  45         107  
  45         2022  
2 45     45   269 use warnings;
  45         110  
  45         4191  
3             package JSON::Schema::Modern::Utilities;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Internal utilities for JSON::Schema::Modern
6              
7             our $VERSION = '0.634';
8              
9 45     45   895 use 5.020;
  45         193  
10 45     45   288 use strictures 2;
  45         352  
  45         1853  
11 45     45   23328 use stable 0.031 'postderef';
  45         782  
  45         316  
12 45     45   8702 use experimental 'signatures';
  45         102  
  45         213  
13 45     45   3421 no autovivification warn => qw(fetch store exists delete);
  45         102  
  45         1954  
14 45     45   4115 use if "$]" >= 5.022, experimental => 're_strict';
  45         95  
  45         1148  
15 45     45   4098 no if "$]" >= 5.031009, feature => 'indirect';
  45         90  
  45         3085  
16 45     45   269 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         121  
  45         2740  
17 45     45   293 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         82  
  45         3144  
18 45     45   271 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         93  
  45         2128  
19 45     45   287 no feature 'switch';
  45         84  
  45         1364  
20 45     45   246 use B;
  45         86  
  45         1398  
21 45     45   209 use Carp qw(carp croak);
  45         89  
  45         3538  
22 45     45   310 use builtin::compat qw(blessed created_as_number);
  45         112  
  45         527  
23 45     45   9341 use Scalar::Util 'looks_like_number';
  45         90  
  45         2860  
24 45     45   283 use Storable 'dclone';
  45         96  
  45         2310  
25 45     45   285 use Feature::Compat::Try;
  45         122  
  45         486  
26 45     45   2789 use Mojo::JSON ();
  45         88  
  45         964  
27 45     45   224 use JSON::PP ();
  45         91  
  45         5201  
28 45     45   218 use Types::Standard qw(Str InstanceOf Enum);
  45         100  
  45         438  
29 45     45   153414 use Mojo::File 'path';
  45         114  
  45         3086  
30 45     45   325 use namespace::clean;
  45         99  
  45         279  
31              
32 45     45   17225 use Exporter 'import';
  45         105  
  45         5587  
33              
34             our @EXPORT_OK = qw(
35             is_type
36             get_type
37             is_bool
38             is_schema
39             is_bignum
40             is_equal
41             is_elements_unique
42             jsonp
43             unjsonp
44             jsonp_set
45             local_annotations
46             canonical_uri
47             E
48             A
49             abort
50             assert_keyword_exists
51             assert_keyword_type
52             assert_pattern
53             assert_uri_reference
54             assert_uri
55             annotate_self
56             sprintf_num
57             true
58             false
59             json_pointer_type
60             canonical_uri_type
61             core_types_type
62             core_formats_type
63             register_schema
64             load_cached_document
65             );
66              
67 45     45   409 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  45         107  
  45         4641  
68 45     45   326 use if HAVE_BUILTIN, experimental => 'builtin';
  45         98  
  45         895  
69              
70 45     45   3503 use constant _BUILTIN_BOOLS => 0;
  45         98  
  45         10596  
71             use constant {
72             _BUILTIN_BOOLS && HAVE_BUILTIN && eval { +require Storable; Storable->VERSION(3.27); 1 }
73 45         284 && Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION(4.38); 1 }
74             ? (true => builtin::true, false => builtin::false)
75             : (true => JSON::PP::true, false => JSON::PP::false)
76 45     45   366 };
  45         94  
77              
78             # supports the six core types, plus integer (which is also a number)
79             # we do NOT check stringy_numbers here -- you must do that in the caller
80             # note that sometimes a value may return true for more than one type, e.g. integer+number,
81             # or number+string, depending on its internal flags.
82             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
83             # behaviour is consistent with get_type() (where integers are also numbers).
84 87613     87613 1 1469282 sub is_type ($type, $value, $config = {}) {
  87613         151432  
  87613         167679  
  87613         172831  
  87613         141266  
85 87613 100       225271 if ($type eq 'null') {
86 113         602 return !(defined $value);
87             }
88 87500 100       201995 if ($type eq 'boolean') {
89 11230         31239 return is_bool($value);
90             }
91 76270 100       179666 if ($type eq 'object') {
92 19889         126030 return ref $value eq 'HASH';
93             }
94 56381 100       140954 if ($type eq 'array') {
95 14092         96603 return ref $value eq 'ARRAY';
96             }
97              
98 42289 100 100     176713 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
99 42273 100       95768 return 0 if not defined $value;
100 42253         290049 my $flags = B::svref_2object(\$value)->FLAGS;
101              
102             # dualvars with the same string and (stringified) numeric value could be either a string or a
103             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
104             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
105             # numified strings do have POK set, so we can tell which one came first.
106              
107 42253 100       138458 if ($type eq 'string') {
108             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
109             return !length ref($value)
110             && !(HAVE_BUILTIN && builtin::is_bool($value))
111             && $flags & B::SVf_POK
112             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
113 45   100 45   22133 || do { no warnings 'numeric'; 0+$value eq $value });
  45         112  
  45         35510  
  30286         421934  
114             }
115              
116 11967 100       44887 if ($type eq 'number') {
117             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
118 8048   100     26665 return is_bignum($value) || created_as_number($value);
119             }
120              
121 3919 50       13388 if ($type eq 'integer') {
122 3919 100       15252 if ($config->{legacy_ints}) {
123             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
124             # therefore 2.0 is NOT an integer
125 17   100     140 return ref($value) eq 'Math::BigInt'
126             || ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) && created_as_number($value);
127             }
128             else {
129             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
130             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
131             # if the exact type is important, or loss of any accuracy is unacceptable
132 3902   100     13158 return is_bignum($value) && $value->is_int
133             # if dualvar, PV and stringified NV/IV must be identical
134             || created_as_number($value) && int($value) == $value;
135             }
136             }
137             }
138              
139 16 100       116 if ($type =~ /^reference to (.+)\z/) {
140 11   33     104 return !blessed($value) && ref($value) eq $1;
141             }
142              
143 5         26 return ref($value) eq $type;
144             }
145              
146             # returns one of the six core types, plus integer
147             # we do NOT check stringy_numbers here -- you must do that in the caller
148             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
149             # behaviour is consistent with is_type().
150 101680     101680 1 862958 sub get_type ($value, $config = {}) {
  101680         176914  
  101680         179850  
  101680         161487  
151 101680 100       438121 return 'object' if ref $value eq 'HASH';
152 29819 100       78509 return 'boolean' if is_bool($value);
153 19282 100       68235 return 'null' if not defined $value;
154 18720 100       48688 return 'array' if ref $value eq 'ARRAY';
155              
156             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
157 16355 100       41149 if (length ref $value) {
158 880         2271 my $ref = ref $value;
159             return $ref eq 'Math::BigInt' ? 'integer'
160 880 100 100     8946 : $ref eq 'Math::BigFloat' ? (!$config->{legacy_ints} && $value->is_int ? 'integer' : 'number')
    100          
    100          
    100          
161             : (defined blessed($value) ? '' : 'reference to ').$ref;
162             }
163              
164 15475         94718 my $flags = B::svref_2object(\$value)->FLAGS;
165              
166             # dualvars with the same string and (stringified) numeric value could be either a string or a
167             # number, and before 5.36 we can't tell the difference, so we choose number because it has been
168             # evaluated as a number already.
169             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
170             # numified strings do have POK set, so we can tell which one came first.
171              
172             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
173             return 'string'
174             if $flags & B::SVf_POK
175             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
176 45 100 100 45   422 || do { no warnings 'numeric'; 0+$value eq $value });
  45   100     114  
  45         129446  
  15475         90069  
177              
178 6696 100       22502 if ($config->{legacy_ints}) {
179             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
180             # therefore 2.0 is NOT an integer
181 316 100 66     2810 return ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) ? 'integer' : 'number'
    50          
182             if created_as_number($value);
183             }
184             else {
185             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
186             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
187             # if the exact type is important, or loss of any accuracy is unacceptable
188 6380 100       44853 return int($value) == $value ? 'integer' : 'number' if created_as_number($value);
    100          
189             }
190              
191             # this might be a scalar with POK|IOK or POK|NOK set
192 15         51 return 'ambiguous type';
193             }
194              
195             # lifted from JSON::MaybeXS
196             # note: unlike builtin::compat::is_bool on older perls, we do not accept
197             # dualvar(0,"") or dualvar(1,"1") because JSON::PP and Cpanel::JSON::XS
198             # do not encode these as booleans.
199 65643     65643 1 111804 sub is_bool ($value) {
  65643         128374  
  65643         99710  
200 65643 100 66     982988 HAVE_BUILTIN and builtin::is_bool($value)
      66        
201             or
202             !!blessed($value)
203             and ($value->isa('JSON::PP::Boolean')
204             or $value->isa('Cpanel::JSON::XS::Boolean')
205             or $value->isa('JSON::XS::Boolean'));
206             }
207              
208 58964     58964 1 104632 sub is_schema ($value) {
  58964         102963  
  58964         94696  
209 58964 100       352122 ref $value eq 'HASH' || is_bool($value);
210             }
211              
212 13509     13509 0 29409 sub is_bignum ($value) {
  13509         26738  
  13509         23214  
213 13509         182811 ref($value) =~ /^Math::Big(?:Int|Float)\z/;
214             }
215              
216             # compares two arbitrary data payloads for equality, as per
217             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
218             # $state hashref supports the following fields:
219             # - scalarref_booleans (input): treats \0 and \1 as boolean values
220             # - stringy_numbers (input): strings will also be compared numerically
221             # - path (output): location of the first difference
222             # - error (output): description of the first difference
223 6454     6454 1 12180 sub is_equal ($x, $y, $state = {}) {
  6454         12441  
  6454         11940  
  6454         11839  
  6454         10858  
224 6454   100     36426 $state->{path} //= '';
225              
226 6454         19687 my @types = map get_type($_), $x, $y;
227              
228 6454 100       30080 $state->{error} = 'ambiguous type encountered', return 0
229             if grep $types[$_] eq 'ambiguous type', 0..1;
230              
231 6451 100       23068 if ($state->{scalarref_booleans}) {
232 88 100       267 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
233 88 100       253 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
234             }
235              
236 6451 100       21013 if ($state->{stringy_numbers}) {
237 16 100 100     126 ($x, $types[0]) = (0+$x, int(0+$x) == $x ? 'integer' : 'number')
    100          
238             if $types[0] eq 'string' and looks_like_number($x);
239              
240 16 100 100     84 ($y, $types[1]) = (0+$y, int(0+$y) == $y ? 'integer' : 'number')
    100          
241             if $types[1] eq 'string' and looks_like_number($y);
242             }
243              
244 6451 100       26731 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
245 5096 100       12751 return 1 if $types[0] eq 'null';
246 5076 100 100     28703 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
247             if $types[0] eq 'string';
248 2475 100 100     18253 ($x == $y and return 1), $state->{error} = "$types[0]s not equal", return 0
249             if grep $types[0] eq $_, qw(boolean number integer);
250              
251 923         2975 my $path = $state->{path};
252 923 100       3193 if ($types[0] eq 'object') {
253 315 100       2054 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
254             if keys %$x != keys %$y;
255              
256 292 100       2898 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
257 9         57 my $pos = substr($s->{path}, 1);
258 9         56 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
259 9         76 return 0;
260             }
261              
262 283         1653 foreach my $property (sort keys %$x) {
263 333         1091 $state->{path} = jsonp($path, $property);
264 333 100       1537 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
265             }
266              
267 156         1766 return 1;
268             }
269              
270 608 50       1993 if ($types[0] eq 'array') {
271 608 100       2184 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
272 597         2637 foreach my $idx (0 .. $x->$#*) {
273 653         2834 $state->{path} = $path.'/'.$idx;
274 653 100       3154 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
275             }
276 401         14322 return 1;
277             }
278              
279 0         0 $state->{error} = 'uh oh', return 0; # should never get here
280             }
281              
282             # checks array elements for uniqueness. short-circuits on first pair of matching elements
283             # $state hashref supports the following fields:
284             # - scalarref_booleans (input): treats \0 and \1 as boolean values
285             # - stringy_numbers (input): strings will also be compared numerically
286             # - path (output): location of the first difference
287             # - error (output): description of the first difference
288             # - equal_indices (output): the indices of identical items
289 3609     3609 1 7584 sub is_elements_unique ($array, $state = {}) {
  3609         7365  
  3609         7134  
  3609         6350  
290 3609         15848 foreach my $idx0 (0 .. $array->$#*-1) {
291 1467         6227 foreach my $idx1 ($idx0+1 .. $array->$#*) {
292 2123 100       10285 if (is_equal($array->[$idx0], $array->[$idx1], $state)) {
293 280 50       6718 push $state->{equal_indices}->@*, $idx0, $idx1 if exists $state->{equal_indices};
294 280         1702 return 0;
295             }
296             }
297             }
298 3329         17411 return 1;
299             }
300              
301             # shorthand for creating and appending json pointers
302             # the first argument is an already-encoded json pointer; remaining arguments are path segments to be
303             # encoded and appended
304             sub jsonp {
305 47966 50 66 47966 1 298289 carp q{first argument to jsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
306 47966         588192 return join('/', shift, map s!~!~0!gr =~ s!/!~1!gr, grep defined, @_);
307             }
308              
309             # splits a json pointer apart into its path segments
310             sub unjsonp {
311 0 0 0 0 1 0 carp q{argument to unjsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
312 0         0 return map s!~0!~!gr =~ s!~1!/!gr, split m!/!, $_[0];
313             }
314              
315             # assigns a value to a data structure at a specific json pointer location
316             # operates destructively, in place, unless the root data or type is being modified
317 37     37 1 14257 sub jsonp_set ($data, $pointer, $value) {
  37         81  
  37         74  
  37         64  
  37         87  
318 37 100       203 if (not grep ref $data eq $_, qw(HASH ARRAY)) {
319 2 100       16 return $value if defined wantarray;
320 1         207 croak 'cannot write into non-reference in void context';
321             }
322              
323             # assigning to the root overwrites existing data
324 35 100       99 if (not length $pointer) {
325 6 100 100     88 if (ref $data eq 'HASH' and ref $value ne 'HASH'
      100        
      100        
326             or ref $data eq 'ARRAY' and ref $value ne 'ARRAY') {
327 4 100       17 return $value if defined wantarray;
328 2         301 croak 'cannot write into reference of different type in void context';
329             }
330              
331 2 100       13 $data->%* = $value->%* if ref $data eq 'HASH';
332 2 100       11 $data->@* = $value->@* if ref $data eq 'ARRAY';
333 2         15 return $data;
334             }
335              
336 29 50       233 my @keys = map +(s!~0!~!gr =~ s!~1!/!gr),
337             (length $pointer ? (split /\//, $pointer, -1) : ($pointer));
338              
339 29 100 66     454 croak 'cannot write hashref into a reference to an array in void context'
      100        
      66        
340             if @keys >= 2 and $keys[1] !~ /^\d+\z/a and ref $data eq 'ARRAY' and not defined wantarray;
341              
342 28         66 shift @keys; # always '', indicating the root
343 28         59 my $curp = \$data;
344              
345 28         91 foreach my $key (@keys) {
346             # if needed, first remove the existing data so we can replace with a new hash key or array index
347 61 100 100     293 undef $curp->$*
      100        
348             if not ref $curp->$*
349             or ref $curp->$* eq 'ARRAY' and $key !~ /^\d+\z/a;
350              
351             # use this existing hash key or array index location, or create new position
352 45     45   1155 use autovivification 'store';
  45         214  
  45         651  
353             $curp = \(
354             ref $curp->$* eq 'HASH' || $key !~ /^\d+\z/a
355 61 100 100     375 ? $curp->$*->{$key}
356             : $curp->$*->[$key]);
357             }
358              
359 28         73 $curp->$* = $value;
360 28         143 return $data;
361             }
362              
363             # returns a reusable Types::Standard type for json pointers
364             # TODO: move this off into its own distribution, see JSON::Schema::Types
365 360     360 1 151152 sub json_pointer_type () { Str->where('!length || m{^/} && !m{~(?![01])}'); }
  360         807  
  360         1661  
366              
367             # a URI without a fragment, or with a json pointer fragment
368 90     90 1 397935 sub canonical_uri_type () {
  90         252  
369 90         570 (InstanceOf['Mojo::URL'])->where(q{!defined($_->fragment) || $_->fragment =~ m{^/} && $_->fragment !~ m{~(?![01])}});
370             }
371              
372             # Validation §7.1-2: "Note that the "type" keyword in this specification defines an "integer" type
373             # which is not part of the data model. Therefore a format attribute can be limited to numbers, but
374             # not specifically to integers."
375 90     90 1 201031 sub core_types_type () {
  90         495  
376 90         782 Enum[qw(null object array boolean string number)];
377             }
378              
379 4     4 1 10 sub core_formats_type () {
  4         8  
380 4         42 Enum[qw(date-time date time duration email idn-email hostname idn-hostname ipv4 ipv6 uri uri-reference iri iri-reference uuid uri-template json-pointer relative-json-pointer regex)];
381             }
382              
383             # simple runtime-wide cache of $ids to schema document objects that are sourced from disk
384             {
385             my $document_cache = {};
386              
387             # Fetches a document from the cache (reading it from disk and creating the document if necessary),
388             # and add it to the evaluator.
389             # Normally this will just be a cache of schemas that are bundled with this distribution or a related
390             # distribution (such as OpenAPI-Modern), as duplicate identifiers are not checked for, unlike for
391             # normal schema additions.
392             # Only JSON-encoded files are supported at this time.
393 114     114 1 285 sub load_cached_document ($evaluator, $uri) {
  114         261  
  114         340  
  114         244  
394 114         423 $uri =~ s/#\z//; # older draft $ids use an empty fragment
395              
396             # see if it already exists as a document in the cache
397 114         23124 my $document = $document_cache->{$uri};
398              
399             # otherwise, load it from disk using our filename cache and create the document
400 114 100 100     23412 if (not $document and my $filename = get_schema_filename($uri)) {
401 68         13961 my $file = path($filename);
402 68 50       2479 die "uri $uri maps to file $file which does not exist" if not -f $file;
403 68         5491 my $schema = $evaluator->_json_decoder->decode($file->slurp);
404              
405             # avoid calling add_schema, which checksums the file to look for duplicates
406 68         18639 $document = JSON::Schema::Modern::Document->new(
407             schema => $schema,
408             evaluator => $evaluator,
409             skip_ref_checks => 1,
410             );
411              
412             # avoid calling add_document, which checks for duplicate identifiers (and would result in an
413             # infinite loop)
414 68 50       403 die JSON::Schema::Modern::Result->new(
415             output_format => $evaluator->output_format,
416             valid => 0,
417             errors => [ $document->errors ],
418             exception => 1,
419             ) if $document->has_errors;
420              
421 68         619 $document_cache->{$uri} = $document;
422             }
423              
424 114 100       23488 return if not $document;
425              
426             # bypass the normal collision checks, to avoid an infinite loop: these documents are presumed safe
427 103         636 $evaluator->_add_resources_unsafe(
428             map +($_->[0] => +{ $_->[1]->%*, document => $document }),
429             $document->resource_pairs
430             );
431              
432 103         80475 return $document;
433             }
434             }
435              
436             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
437              
438             # get all annotations produced for the current instance data location (that are visible to this
439             # schema location) - remember these are hashrefs, not Annotation objects
440 1395     1395 0 2582 sub local_annotations ($state) {
  1395         2743  
  1395         2444  
441 1395         11004 grep $_->{instance_location} eq $state->{data_path}, $state->{annotations}->@*;
442             }
443              
444             # shorthand for finding the current uri of the present schema location
445             # ensure that this code is kept consistent with the absolute_keyword_location builder in ResultNode
446             # Note that this may not be canonical if keyword_path has not yet been reset via the processing of a
447             # local identifier keyword (e.g. '$id').
448 34239     34239 0 71357 sub canonical_uri ($state, @extra_path) {
  34239         66243  
  34239         60721  
  34239         54389  
449 34239 100 66     247541 return $state->{initial_schema_uri} if not @extra_path and not length($state->{keyword_path});
450 12796         72524 my $uri = $state->{initial_schema_uri}->clone;
451 12796 50 100     1245303 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{keyword_path}, @extra_path) : $state->{keyword_path});
452 12796 50       139857 undef $fragment if not length($fragment);
453 12796         40923 $uri->fragment($fragment);
454 12796         98862 $uri;
455             }
456              
457             # shorthand for creating error objects
458             # uses these keys from $state:
459             # - initial_schema_uri
460             # - keyword (optional)
461             # - data_path
462             # - traversed_keyword_path
463             # - keyword_path
464             # - _keyword_path_suffix (optional)
465             # - errors
466             # - exception (optional; set by abort())
467             # - recommended_response (optional)
468             # - depth
469             # - traverse (boolean, used for mode)
470             # returns defined-false, so callers can use 'return;' to differentiate between
471             # failed-with-no-error from failed-with-error.
472 13954     13954 0 74735 sub E ($state, $error_string, @args) {
  13954         28273  
  13954         27512  
  13954         33253  
  13954         22825  
473 13954 50       45955 croak 'E called in void context' if not defined wantarray;
474              
475             # sometimes the keyword shouldn't be at the very end of the schema path
476 13954         68651 my $sps = delete $state->{_keyword_path_suffix};
477 13954 100 100     92724 my @keyword_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
478              
479             # we store the absolute uri in unresolved form until needed,
480             # and perform the rest of the calculations later.
481 13954   100     108356 my $uri = [ $state->@{qw(initial_schema_uri keyword_path)}, $state->{keyword}//(), @keyword_path_suffix ];
482              
483             my $keyword_location = $state->{traversed_keyword_path}
484 13954         98301 .jsonp($state->@{qw(keyword_path keyword)}, @keyword_path_suffix);
485              
486 13954         124276 require JSON::Schema::Modern::Error;
487             push $state->{errors}->@*, JSON::Schema::Modern::Error->new(
488             depth => $state->{depth} // 0,
489             keyword => $state->{keyword},
490             $state->{traverse} ? () : (instance_location => $state->{data_path}),
491             keyword_location => $keyword_location,
492             # we calculate absolute_keyword_location when instantiating the Error object for Result
493             _uri => $uri,
494             error => @args ? sprintf($error_string, @args) : $error_string,
495             exception => $state->{exception},
496             ($state->%{recommended_response})x!!$state->{recommended_response},
497 13954 100 50     717752 mode => $state->{traverse} ? 'traverse' : 'evaluate',
    100          
    100          
498             );
499              
500 13954         122595 return 0;
501             }
502              
503             # shorthand for creating annotations
504             # uses these keys from $state:
505             # - initial_schema_uri
506             # - keyword (mandatory)
507             # - data_path
508             # - traversed_keyword_path
509             # - keyword_path
510             # - annotations
511             # - collect_annotations
512             # - _unknown (boolean)
513             # - depth
514 12020     12020 0 22638 sub A ($state, $annotation) {
  12020         21895  
  12020         25303  
  12020         19711  
515             # even if the user requested annotations, we only collect them for later drafts
516             # ..but we always collect them if the lowest bit is set, indicating the presence of unevaluated*
517             # keywords necessary for accurate validation
518             return 1 if not ($state->{collect_annotations}
519 12020 100       111128 & ($state->{specification_version} =~ /^draft[467]\z/ ? ~(1<<8) : ~0));
    100          
520              
521             # we store the absolute uri in unresolved form until needed,
522             # and perform the rest of the calculations later.
523 2638         12899 my $uri = [ $state->@{qw(initial_schema_uri keyword_path keyword)} ];
524              
525 2638         12622 my $keyword_location = $state->{traversed_keyword_path}.jsonp($state->@{qw(keyword_path keyword)});
526              
527             push $state->{annotations}->@*, {
528             depth => $state->{depth} // 0,
529             keyword => $state->{keyword},
530             instance_location => $state->{data_path},
531             keyword_location => $keyword_location,
532             # we calculate absolute_keyword_location when instantiating the Annotation object for Result
533             _uri => $uri,
534             annotation => $annotation,
535 2638 100 50     38030 $state->{_unknown} ? (unknown => 1) : (),
536             };
537              
538 2638         8781 return 1;
539             }
540              
541             # creates an error object, but also aborts evaluation immediately
542             # only this error is returned, because other errors on the stack might not actually be "real"
543             # errors (consider if we were in the middle of evaluating a "not" or "if").
544             # Therefore this is only appropriate during the evaluation phase, not the traverse phase.
545 34     34 0 425 sub abort ($state, $error_string, @args) {
  34         88  
  34         86  
  34         88  
  34         69  
546 34         655 ()= E({ %$state, exception => 1 }, $error_string, @args);
547 34 50       361 croak 'abort() called during traverse' if $state->{traverse};
548 34         632 die pop $state->{errors}->@*;
549             }
550              
551 0     0 0 0 sub assert_keyword_exists ($state, $schema) {
  0         0  
  0         0  
  0         0  
552 0 0       0 croak 'assert_keyword_exists called in void context' if not defined wantarray;
553 0 0       0 return E($state, '%s keyword is required', $state->{keyword}) if not exists $schema->{$state->{keyword}};
554 0         0 return 1;
555             }
556              
557 42323     42323 0 73973 sub assert_keyword_type ($state, $schema, $type) {
  42323         74202  
  42323         75454  
  42323         78248  
  42323         62868  
558 42323 50       104210 croak 'assert_keyword_type called in void context' if not defined wantarray;
559 42323 100       176333 return 1 if is_type($type, $schema->{$state->{keyword}});
560 17 100       172 E($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
561             }
562              
563 2875     2875 0 6964 sub assert_pattern ($state, $pattern) {
  2875         6094  
  2875         6463  
  2875         5748  
564 2875 50       9412 croak 'assert_pattern called in void context' if not defined wantarray;
565 2875         6999 try {
566 2875     0   29447 local $SIG{__WARN__} = sub { die @_ };
  0         0  
567 2875         54555 qr/$pattern/;
568             }
569 3         20 catch ($e) { return E($state, $e); };
570 2872         28300 return 1;
571             }
572              
573             # this is only suitable for checking URIs within schemas themselves
574             # note that we cannot use $state->{specification_version} to more tightly constrain the plain-name
575             # fragment syntax, as we could be checking a $ref to a schema using a different version
576 5886     5886 0 11874 sub assert_uri_reference ($state, $schema) {
  5886         10408  
  5886         9883  
  5886         9137  
577 5886 50       13960 croak 'assert_uri_reference called in void context' if not defined wantarray;
578              
579 5886         23663 my $string = $schema->{$state->{keyword}};
580             return E($state, '%s value is not a valid URI-reference', $state->{keyword})
581             # see also uri-reference format sub
582 5886 100 66     32175 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      100        
      100        
583             or $string =~ /[^[:ascii:]]/ # ascii characters only
584             or $string =~ /#/ # no fragment, except...
585             and $string !~ m{#\z} # allow empty fragment
586             and $string !~ m{#[A-Za-z_][A-Za-z0-9_:.-]*\z} # allow plain-name fragment, superset of all drafts
587             and $string !~ m{#/(?:[^~]|~[01])*\z}; # allow json pointer fragment
588              
589 5852         2646806 return 1;
590             }
591              
592             # this is only suitable for checking URIs within schemas themselves,
593             # which have fragments consisting of plain names (anchors) or json pointers
594 6199     6199 0 14756 sub assert_uri ($state, $schema, $override = undef) {
  6199         12238  
  6199         11408  
  6199         13095  
  6199         10202  
595 6199 50       19485 croak 'assert_uri called in void context' if not defined wantarray;
596              
597 6199   33     19928 my $string = $override // $schema->{$state->{keyword}};
598 6199         36597 my $uri = Mojo::URL->new($string);
599              
600 6199 100 66     708851 return E($state, '"%s" is not a valid URI', $string)
      100        
      100        
      66        
      66        
      66        
601             # see also uri format sub
602             if fc($uri->to_unsafe_string) ne fc($string)
603             or $string =~ /[^[:ascii:]]/ # ascii characters only
604             or not $uri->is_abs # must have a scheme
605             or $string =~ /#/ # no fragment, except...
606             and $string !~ m{#\z} # empty fragment
607             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*\z} # plain-name fragment
608             and $string !~ m{#/(?:[^~]|~[01])*\z}; # json pointer fragment
609              
610 6183         1731999 return 1;
611             }
612              
613             # produces an annotation whose value is the same as that of the current schema keyword
614             # makes a copy as this is passed back to the user, who cannot be trusted to not mutate it
615 1138     1138 0 2168 sub annotate_self ($state, $schema) {
  1138         1943  
  1138         1908  
  1138         1866  
616             A($state, ref $schema->{$state->{keyword}} ? dclone($schema->{$state->{keyword}})
617 1138 100       17821 : $schema->{$state->{keyword}});
618             }
619              
620             # use original value as stored in the NV, without losing precision
621 1305     1305 0 2969 sub sprintf_num ($value) {
  1305         2962  
  1305         2613  
622 1305 100       3518 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
623             }
624              
625             {
626             # simple runtime-wide cache of $ids to filenames that are sourced from disk
627             my $schema_filename_cache = {};
628              
629             # adds a mapping from a URI to an absolute filename in the global runtime
630             # (available to all instances of the evaluator running in the same process).
631 945     945 0 1372 sub register_schema ($uri, $filename) {
  945         1382  
  945         1319  
  945         1184  
632 945         3805 $schema_filename_cache->{$uri} = $filename;
633             }
634              
635 1998     1998 0 3820 sub get_schema_filename ($uri) {
  1998         4009  
  1998         3203  
636 1998         9691 $schema_filename_cache->{$uri};
637             }
638             }
639              
640             1;
641              
642             __END__