File Coverage

blib/lib/JSON/Schema/Modern/Utilities.pm
Criterion Covered Total %
statement 352 368 95.6
branch 162 186 87.1
condition 117 138 84.7
subroutine 60 64 93.7
pod 15 30 50.0
total 706 786 89.8


line stmt bran cond sub pod time code
1 45     45   247 use strict;
  45         81  
  45         1483  
2 45     45   213 use warnings;
  45         73  
  45         3198  
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.637';
8              
9 45     45   621 use 5.020;
  45         149  
10 45     45   181 use strictures 2;
  45         257  
  45         1359  
11 45     45   14913 use stable 0.031 'postderef';
  45         544  
  45         209  
12 45     45   5659 use experimental 'signatures';
  45         69  
  45         138  
13 45     45   1906 no autovivification warn => qw(fetch store exists delete);
  45         89  
  45         215  
14 45     45   2862 use if "$]" >= 5.022, experimental => 're_strict';
  45         182  
  45         813  
15 45     45   2710 no if "$]" >= 5.031009, feature => 'indirect';
  45         67  
  45         2147  
16 45     45   187 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         65  
  45         1665  
17 45     45   162 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         60  
  45         1737  
18 45     45   181 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         68  
  45         1431  
19 45     45   162 no feature 'switch';
  45         86  
  45         943  
20 45     45   176 use B;
  45         71  
  45         997  
21 45     45   140 use Carp qw(carp croak);
  45         62  
  45         2277  
22 45     45   187 use builtin::compat qw(blessed created_as_number);
  45         107  
  45         449  
23 45     45   6730 use Scalar::Util 'looks_like_number';
  45         115  
  45         2016  
24 45     45   206 use Storable 'dclone';
  45         65  
  45         1469  
25 45     45   186 use Feature::Compat::Try;
  45         62  
  45         361  
26 45     45   1916 use Mojo::JSON ();
  45         66  
  45         564  
27 45     45   182 use Mojo::JSON::Pointer ();
  45         73  
  45         502  
28 45     45   145 use JSON::PP ();
  45         80  
  45         846  
29 45     45   184 use Types::Standard qw(Str InstanceOf Enum);
  45         81  
  45         6130  
30 45     45   104002 use Mojo::File 'path';
  45         88  
  45         2342  
31 45     45   205 use namespace::clean;
  45         66  
  45         242  
32              
33 45     45   11871 use Exporter 'import';
  45         76  
  45         3892  
34              
35             our @EXPORT_OK = qw(
36             is_type
37             get_type
38             is_bool
39             is_schema
40             is_bignum
41             is_equal
42             is_elements_unique
43             jsonp
44             unjsonp
45             jsonp_get
46             jsonp_set
47             local_annotations
48             canonical_uri
49             E
50             A
51             abort
52             assert_keyword_exists
53             assert_keyword_type
54             assert_pattern
55             assert_uri_reference
56             assert_uri
57             annotate_self
58             sprintf_num
59             true
60             false
61             json_pointer_type
62             canonical_uri_type
63             core_types_type
64             core_formats_type
65             register_schema
66             load_cached_document
67             );
68              
69 45     45   229 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  45         67  
  45         3466  
70 45     45   211 use if HAVE_BUILTIN, experimental => 'builtin';
  45         74  
  45         730  
71              
72 45     45   2218 use constant _BUILTIN_BOOLS => 0;
  45         73  
  45         6381  
73             use constant {
74             _BUILTIN_BOOLS && HAVE_BUILTIN && eval { +require Storable; Storable->VERSION(3.27); 1 }
75 45         222 && Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION(4.38); 1 }
76             ? (true => builtin::true, false => builtin::false)
77             : (true => JSON::PP::true, false => JSON::PP::false)
78 45     45   232 };
  45         66  
79              
80             # supports the six core types, plus integer (which is also a number)
81             # we do NOT check stringy_numbers here -- you must do that in the caller
82             # note that sometimes a value may return true for more than one type, e.g. integer+number,
83             # or number+string, depending on its internal flags.
84             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
85             # behaviour is consistent with get_type() (where integers are also numbers).
86 87648     87648 1 999001 sub is_type ($type, $value, $config = {}) {
  87648         111898  
  87648         110072  
  87648         115852  
  87648         100710  
87 87648 100       145000 if ($type eq 'null') {
88 113         446 return !(defined $value);
89             }
90 87535 100       139180 if ($type eq 'boolean') {
91 11231         21097 return is_bool($value);
92             }
93 76304 100       118509 if ($type eq 'object') {
94 19901         81109 return ref $value eq 'HASH';
95             }
96 56403 100       96139 if ($type eq 'array') {
97 14093         61636 return ref $value eq 'ARRAY';
98             }
99              
100 42310 100 100     123172 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
101 42294 100       68268 return 0 if not defined $value;
102 42274         201689 my $flags = B::svref_2object(\$value)->FLAGS;
103              
104             # dualvars with the same string and (stringified) numeric value could be either a string or a
105             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
106             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
107             # numified strings do have POK set, so we can tell which one came first.
108              
109 42274 100       93357 if ($type eq 'string') {
110             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
111             return !length ref($value)
112             && !(HAVE_BUILTIN && builtin::is_bool($value))
113             && $flags & B::SVf_POK
114             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
115 45   100 45   14319 || do { no warnings 'numeric'; 0+$value eq $value });
  45         108  
  45         24143  
  30307         270625  
116             }
117              
118 11967 100       25318 if ($type eq 'number') {
119             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
120 8048   100     15234 return is_bignum($value) || created_as_number($value);
121             }
122              
123 3919 50       8464 if ($type eq 'integer') {
124 3919 100       9829 if ($config->{legacy_ints}) {
125             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
126             # therefore 2.0 is NOT an integer
127 17   100     118 return ref($value) eq 'Math::BigInt'
128             || ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) && created_as_number($value);
129             }
130             else {
131             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
132             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
133             # if the exact type is important, or loss of any accuracy is unacceptable
134 3902   100     8738 return is_bignum($value) && $value->is_int
135             # if dualvar, PV and stringified NV/IV must be identical
136             || created_as_number($value) && int($value) == $value;
137             }
138             }
139             }
140              
141 16 100       71 if ($type =~ /^reference to (.+)\z/) {
142 11   33     77 return !blessed($value) && ref($value) eq $1;
143             }
144              
145 5         25 return ref($value) eq $type;
146             }
147              
148             # returns one of the six core types, plus integer
149             # we do NOT check stringy_numbers here -- you must do that in the caller
150             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
151             # behaviour is consistent with is_type().
152 122314     122314 1 762376 sub get_type ($value, $config = {}) {
  122314         148010  
  122314         149877  
  122314         140396  
153 122314 100       327198 return 'object' if ref $value eq 'HASH';
154 47718 100       70142 return 'boolean' if is_bool($value);
155 36318 100       71015 return 'null' if not defined $value;
156 35732 100       59707 return 'array' if ref $value eq 'ARRAY';
157              
158             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
159 29867 100       49151 if (length(my $ref = ref $value)) {
160             return $ref eq 'Math::BigInt' ? 'integer'
161 880 100 100     5879 : $ref eq 'Math::BigFloat' ? (!$config->{legacy_ints} && $value->is_int ? 'integer' : 'number')
    100          
    100          
    100          
162             : (defined blessed($value) ? '' : 'reference to ').$ref;
163             }
164              
165 28987         88351 my $flags = B::svref_2object(\$value)->FLAGS;
166              
167             # dualvars with the same string and (stringified) numeric value could be either a string or a
168             # number, and before 5.36 we can't tell the difference, so we choose number because it has been
169             # evaluated as a number already.
170             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
171             # numified strings do have POK set, so we can tell which one came first.
172              
173             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
174             return 'string'
175             if $flags & B::SVf_POK
176             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
177 45 100 100 45   799 || do { no warnings 'numeric'; 0+$value eq $value });
  45   100     256  
  45         89679  
  28987         99689  
178              
179 6904 100       14515 if ($config->{legacy_ints}) {
180             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
181             # therefore 2.0 is NOT an integer
182 316 100 66     2074 return ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) ? 'integer' : 'number'
    50          
183             if created_as_number($value);
184             }
185             else {
186             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
187             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
188             # if the exact type is important, or loss of any accuracy is unacceptable
189 6588 100       31045 return int($value) == $value ? 'integer' : 'number' if created_as_number($value);
    100          
190             }
191              
192             # this might be a scalar with POK|IOK or POK|NOK set
193 15         50 return 'ambiguous type';
194             }
195              
196             # lifted from JSON::MaybeXS
197             # note: unlike builtin::compat::is_bool on older perls, we do not accept
198             # dualvar(0,"") or dualvar(1,"1") because JSON::PP and Cpanel::JSON::XS
199             # do not encode these as booleans.
200 83548     83548 1 101433 sub is_bool ($value) {
  83548         103684  
  83548         89911  
201 83548 100 66     685068 HAVE_BUILTIN and builtin::is_bool($value)
      66        
202             or
203             !!blessed($value)
204             and ($value->isa('JSON::PP::Boolean')
205             or $value->isa('Cpanel::JSON::XS::Boolean')
206             or $value->isa('JSON::XS::Boolean'));
207             }
208              
209 58973     58973 1 72091 sub is_schema ($value) {
  58973         70947  
  58973         71171  
210 58973 100       226178 ref $value eq 'HASH' || is_bool($value);
211             }
212              
213 13509     13509 0 17259 sub is_bignum ($value) {
  13509         17765  
  13509         16101  
214 13509         110457 ref($value) =~ /^Math::Big(?:Int|Float)\z/;
215             }
216              
217             # compares two arbitrary data payloads for equality, as per
218             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
219             # $state hashref supports the following fields:
220             # - scalarref_booleans (input): treats \0 and \1 as boolean values
221             # - stringy_numbers (input): strings will also be compared numerically
222             # - path (output): location of the first difference
223             # - error (output): description of the first difference
224 16758     16758 1 134922 sub is_equal ($x, $y, $state = {}) {
  16758         20290  
  16758         19077  
  16758         18718  
  16758         17224  
225 16758   100     43999 $state->{path} //= '';
226              
227 16758         27776 my @types = map get_type($_), $x, $y;
228              
229 16758 100       38837 $state->{error} = 'ambiguous type encountered', return 0
230             if grep $types[$_] eq 'ambiguous type', 0..1;
231              
232 16755 100       30066 if ($state->{scalarref_booleans}) {
233 88 100       163 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
234 88 100       151 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
235             }
236              
237 16755 100       27751 if ($state->{stringy_numbers}) {
238 16 100 100     93 ($x, $types[0]) = (0+$x, int(0+$x) == $x ? 'integer' : 'number')
    100          
239             if $types[0] eq 'string' and looks_like_number($x);
240              
241 16 100 100     65 ($y, $types[1]) = (0+$y, int(0+$y) == $y ? 'integer' : 'number')
    100          
242             if $types[1] eq 'string' and looks_like_number($y);
243             }
244              
245 16755 100       30393 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
246 15400 100       22512 return 1 if $types[0] eq 'null';
247 15368 100 100     41244 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
248             if $types[0] eq 'string';
249 6119 100 100     20176 ($x == $y and return 1), $state->{error} = "$types[0]s not equal", return 0
250             if grep $types[0] eq $_, qw(boolean number integer);
251              
252 4031         6485 my $path = $state->{path};
253 4031 100       6689 if ($types[0] eq 'object') {
254 1673 100       4533 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
255             if keys %$x != keys %$y;
256              
257 1650 100       11280 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
258 9         35 my $pos = substr($s->{path}, 1);
259 9         47 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
260 9         64 return 0;
261             }
262              
263 1641         5289 foreach my $property (sort keys %$x) {
264 3938         5750 $state->{path} = jsonp($path, $property);
265 3938 100       7763 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
266             }
267              
268 1514         8550 return 1;
269             }
270              
271 2358 50       4072 if ($types[0] eq 'array') {
272 2358 100       4248 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
273 2347         5274 foreach my $idx (0 .. $x->$#*) {
274 5453         9509 $state->{path} = $path.'/'.$idx;
275 5453 100       10602 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
276             }
277 2151         13811 return 1;
278             }
279              
280 0         0 $state->{error} = 'uh oh', return 0; # should never get here
281             }
282              
283             # checks array elements for uniqueness. short-circuits on first pair of matching elements
284             # $state hashref supports the following fields:
285             # - scalarref_booleans (input): treats \0 and \1 as boolean values
286             # - stringy_numbers (input): strings will also be compared numerically
287             # - path (output): location of the first difference
288             # - error (output): description of the first difference
289             # - equal_indices (output): the indices of identical items
290 3609     3609 1 5387 sub is_elements_unique ($array, $state = {}) {
  3609         5117  
  3609         5294  
  3609         4905  
291 3609         11251 foreach my $idx0 (0 .. $array->$#*-1) {
292 1467         4027 foreach my $idx1 ($idx0+1 .. $array->$#*) {
293 2123 100       6824 if (is_equal($array->[$idx0], $array->[$idx1], $state)) {
294 280 50       4805 push $state->{equal_indices}->@*, $idx0, $idx1 if exists $state->{equal_indices};
295 280         1215 return 0;
296             }
297             }
298             }
299 3329         11861 return 1;
300             }
301              
302             # shorthand for creating and appending json pointers
303             # the first argument is an already-encoded json pointer; remaining arguments are path segments to be
304             # encoded and appended
305             sub jsonp {
306 51580 50 66 51580 1 216798 carp q{first argument to jsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
307 51580         402261 return join('/', shift, map s!~!~0!gr =~ s!/!~1!gr, grep defined, @_);
308             }
309              
310             # splits a json pointer apart into its path segments
311             sub unjsonp {
312 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 '/';
313 0         0 return map s!~0!~!gr =~ s!~1!/!gr, split m!/!, $_[0];
314             }
315              
316 0     0 1 0 sub jsonp_get ($data, $pointer) {
  0         0  
  0         0  
  0         0  
317 0         0 Mojo::JSON::Pointer->new($data)->get($pointer);
318             }
319              
320             # assigns a value to a data structure at a specific json pointer location
321             # operates destructively, in place, unless the root data or type is being modified
322 17470     17470 1 47019 sub jsonp_set ($data, $pointer, $value) {
  17470         31208  
  17470         25270  
  17470         25311  
  17470         22677  
323 17470 100 100     121257 croak 'cannot write into a non-reference in void context'
324             if not grep ref $data eq $_, qw(HASH ARRAY) and not defined wantarray;
325              
326             # assigning to the root overwrites existing data
327 17468 100       41672 if (not length $pointer) {
328 17412 100 100     51695 if (not ref $data or ref $data ne ref $value) {
329 17410 100       90614 return $value if defined wantarray;
330 2         204 croak 'cannot write into a reference of a different type in void context';
331             }
332              
333 2 100       6 if (ref $value eq 'HASH') {
334 1 50       3 $data = {} if not ref $data;
335 1         4 $data->%* = $value->%*;
336             }
337 2 100       5 if (ref $value eq 'ARRAY') {
338 1 50       5 $data = [] if not ref $data;
339 1         4 $data->@* = $value->@*;
340             }
341              
342 2         3 return $data;
343             }
344              
345 56 50       381 my @keys = map +(s!~0!~!gr =~ s!~1!/!gr),
346             (length $pointer ? (split /\//, $pointer, -1) : ($pointer));
347              
348 56 100 66     554 croak 'cannot write a hashref into a reference to an array in void context'
      100        
      66        
349             if @keys >= 2 and $keys[1] !~ /^(?:\d+|-)\z/a and ref $data eq 'ARRAY' and not defined wantarray;
350              
351 55         85 shift @keys; # always '', indicating the root
352 55         117 my $curp = \$data;
353              
354 55         98 foreach my $key (@keys) {
355             # if needed, first remove the existing data so we can replace with a new hash key or array index
356 113 100 100     329 undef $curp->$*
      100        
357             if not ref $curp->$*
358             or ref $curp->$* eq 'ARRAY' and $key !~ /^(?:\d+|-)\z/a;
359              
360             # use this existing hash key or array index location, or create new position
361 45     45   402 use autovivification 'store';
  45         186  
  45         304  
362             $curp = \(
363             ref $curp->$* eq 'HASH' || $key !~ /^(?:\d+|-)\z/a
364 113 100 100     484 ? $curp->$*->{$key}
    100          
365             : $key =~ /^\d+\z/a
366             ? $curp->$*->[$key]
367             : $curp->$*->[$curp->$*->$#* + 1]);
368             }
369              
370 55         96 $curp->$* = $value;
371 55         319 return $data;
372             }
373              
374             # returns a reusable Types::Standard type for json pointers
375             # TODO: move this off into its own distribution, see JSON::Schema::Types
376 360     360 1 99362 sub json_pointer_type () { Str->where('!length || m{^/} && !m{~(?![01])}'); }
  360         653  
  360         1263  
377              
378             # a URI without a fragment, or with a json pointer fragment
379 90     90 1 257714 sub canonical_uri_type () {
  90         193  
380 90         425 (InstanceOf['Mojo::URL'])->where(q{!defined($_->fragment) || $_->fragment =~ m{^/} && $_->fragment !~ m{~(?![01])}});
381             }
382              
383             # Validation §7.1-2: "Note that the "type" keyword in this specification defines an "integer" type
384             # which is not part of the data model. Therefore a format attribute can be limited to numbers, but
385             # not specifically to integers."
386 90     90 1 119323 sub core_types_type () {
  90         162  
387 90         388 Enum[qw(null object array boolean string number)];
388             }
389              
390 4     4 1 7 sub core_formats_type () {
  4         4  
391 4         25 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)];
392             }
393              
394             # simple runtime-wide cache of $ids to schema document objects that are sourced from disk
395             {
396             my $document_cache = {};
397              
398             # Fetches a document from the cache (reading it from disk and creating the document if necessary),
399             # and add it to the evaluator.
400             # Normally this will just be a cache of schemas that are bundled with this distribution or a related
401             # distribution (such as OpenAPI-Modern), as duplicate identifiers are not checked for, unlike for
402             # normal schema additions.
403             # Only JSON-encoded files are supported at this time.
404 114     114 1 208 sub load_cached_document ($evaluator, $uri) {
  114         188  
  114         188  
  114         207  
405 114         297 $uri =~ s/#\z//; # older draft $ids use an empty fragment
406              
407             # see if it already exists as a document in the cache
408 114         13934 my $document = $document_cache->{$uri};
409              
410             # otherwise, load it from disk using our filename cache and create the document
411 114 100 100     13451 if (not $document and my $filename = get_schema_filename($uri)) {
412 68         8182 my $file = path($filename);
413 68 50       1887 die "uri $uri maps to file $file which does not exist" if not -f $file;
414 68         4934 my $schema = $evaluator->_json_decoder->decode($file->slurp);
415              
416             # avoid calling add_schema, which checksums the file to look for duplicates
417 68         14508 $document = JSON::Schema::Modern::Document->new(
418             schema => $schema,
419             evaluator => $evaluator,
420             skip_ref_checks => 1,
421             );
422              
423             # avoid calling add_document, which checks for duplicate identifiers (and would result in an
424             # infinite loop)
425 68 50       299 die JSON::Schema::Modern::Result->new(
426             output_format => $evaluator->output_format,
427             valid => 0,
428             errors => [ $document->errors ],
429             exception => 1,
430             ) if $document->has_errors;
431              
432 68         460 $document_cache->{$uri} = $document;
433             }
434              
435 114 100       14650 return if not $document;
436              
437             # bypass the normal collision checks, to avoid an infinite loop: these documents are presumed safe
438 103         482 $evaluator->_add_resources_unsafe(
439             map +($_->[0] => +{ $_->[1]->%*, document => $document }),
440             $document->resource_pairs
441             );
442              
443 103         58721 return $document;
444             }
445             }
446              
447             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
448              
449             # get all annotations produced for the current instance data location (that are visible to this
450             # schema location) - remember these are hashrefs, not Annotation objects
451 1395     1395 0 1966 sub local_annotations ($state) {
  1395         1993  
  1395         1862  
452 1395         6774 grep $_->{instance_location} eq $state->{data_path}, $state->{annotations}->@*;
453             }
454              
455             # shorthand for finding the current uri of the present schema location
456             # ensure that this code is kept consistent with the absolute_keyword_location builder in ResultNode
457             # Note that this may not be canonical if keyword_path has not yet been reset via the processing of a
458             # local identifier keyword (e.g. '$id').
459 34245     34245 0 49826 sub canonical_uri ($state, @extra_path) {
  34245         42934  
  34245         41193  
  34245         41753  
460 34245 100 66     168479 return $state->{initial_schema_uri} if not @extra_path and not length($state->{keyword_path});
461 12799         51576 my $uri = $state->{initial_schema_uri}->clone;
462 12799 50 100     869183 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{keyword_path}, @extra_path) : $state->{keyword_path});
463 12799 50       94477 undef $fragment if not length($fragment);
464 12799         30952 $uri->fragment($fragment);
465 12799         67079 $uri;
466             }
467              
468             # shorthand for creating error objects
469             # uses these keys from $state:
470             # - initial_schema_uri
471             # - keyword (optional)
472             # - data_path
473             # - traversed_keyword_path
474             # - keyword_path
475             # - _keyword_path_suffix (optional)
476             # - errors
477             # - exception (optional; set by abort())
478             # - recommended_response (optional)
479             # - depth
480             # - traverse (boolean, used for mode)
481             # returns defined-false, so callers can use 'return;' to differentiate between
482             # failed-with-no-error from failed-with-error.
483 13954     13954 0 48409 sub E ($state, $error_string, @args) {
  13954         18603  
  13954         20013  
  13954         22597  
  13954         16989  
484 13954 50       29977 croak 'E called in void context' if not defined wantarray;
485              
486             # sometimes the keyword shouldn't be at the very end of the schema path
487 13954         34176 my $sps = delete $state->{_keyword_path_suffix};
488 13954 100 100     60054 my @keyword_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
489              
490             # we store the absolute uri in unresolved form until needed,
491             # and perform the rest of the calculations later.
492 13954   100     61699 my $uri = [ $state->@{qw(initial_schema_uri keyword_path)}, $state->{keyword}//(), @keyword_path_suffix ];
493              
494             my $keyword_location = $state->{traversed_keyword_path}
495 13954         50890 .jsonp($state->@{qw(keyword_path keyword)}, @keyword_path_suffix);
496              
497 13954         88966 require JSON::Schema::Modern::Error;
498             push $state->{errors}->@*, JSON::Schema::Modern::Error->new(
499             depth => $state->{depth} // 0,
500             keyword => $state->{keyword},
501             $state->{traverse} ? () : (instance_location => $state->{data_path}),
502             keyword_location => $keyword_location,
503             # we calculate absolute_keyword_location when instantiating the Error object for Result
504             _uri => $uri,
505             error => @args ? sprintf($error_string, @args) : $error_string,
506             exception => $state->{exception},
507             ($state->%{recommended_response})x!!$state->{recommended_response},
508 13954 100 50     464268 mode => $state->{traverse} ? 'traverse' : 'evaluate',
    100          
    100          
509             );
510              
511 13954         84013 return 0;
512             }
513              
514             # shorthand for creating annotations
515             # uses these keys from $state:
516             # - initial_schema_uri
517             # - keyword (mandatory)
518             # - data_path
519             # - traversed_keyword_path
520             # - keyword_path
521             # - annotations
522             # - collect_annotations
523             # - _unknown (boolean)
524             # - depth
525 12028     12028 0 15963 sub A ($state, $annotation) {
  12028         15984  
  12028         15869  
  12028         15607  
526             # even if the user requested annotations, we only collect them for later drafts
527             # ..but we always collect them if the lowest bit is set, indicating the presence of unevaluated*
528             # keywords necessary for accurate validation
529             return 1 if not ($state->{collect_annotations}
530 12028 100       71527 & ($state->{specification_version} =~ /^draft[467]\z/ ? ~(1<<8) : ~0));
    100          
531              
532             # we store the absolute uri in unresolved form until needed,
533             # and perform the rest of the calculations later.
534 2638         9065 my $uri = [ $state->@{qw(initial_schema_uri keyword_path keyword)} ];
535              
536 2638         8882 my $keyword_location = $state->{traversed_keyword_path}.jsonp($state->@{qw(keyword_path keyword)});
537              
538             push $state->{annotations}->@*, {
539             depth => $state->{depth} // 0,
540             keyword => $state->{keyword},
541             instance_location => $state->{data_path},
542             keyword_location => $keyword_location,
543             # we calculate absolute_keyword_location when instantiating the Annotation object for Result
544             _uri => $uri,
545             annotation => $annotation,
546 2638 100 50     24780 $state->{_unknown} ? (unknown => 1) : (),
547             };
548              
549 2638         5857 return 1;
550             }
551              
552             # creates an error object, but also aborts evaluation immediately
553             # only this error is returned, because other errors on the stack might not actually be "real"
554             # errors (consider if we were in the middle of evaluating a "not" or "if").
555             # Therefore this is only appropriate during the evaluation phase, not the traverse phase.
556 34     34 0 266 sub abort ($state, $error_string, @args) {
  34         63  
  34         58  
  34         64  
  34         67  
557 34         600 ()= E({ %$state, exception => 1 }, $error_string, @args);
558 34 50       207 croak 'abort() called during traverse' if $state->{traverse};
559 34         414 die pop $state->{errors}->@*;
560             }
561              
562 0     0 0 0 sub assert_keyword_exists ($state, $schema) {
  0         0  
  0         0  
  0         0  
563 0 0       0 croak 'assert_keyword_exists called in void context' if not defined wantarray;
564 0 0       0 return E($state, '%s keyword is required', $state->{keyword}) if not exists $schema->{$state->{keyword}};
565 0         0 return 1;
566             }
567              
568 42348     42348 0 54324 sub assert_keyword_type ($state, $schema, $type) {
  42348         49614  
  42348         49379  
  42348         54589  
  42348         45884  
569 42348 50       75022 croak 'assert_keyword_type called in void context' if not defined wantarray;
570 42348 100       117939 return 1 if is_type($type, $schema->{$state->{keyword}});
571 17 100       92 E($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
572             }
573              
574 2875     2875 0 7845 sub assert_pattern ($state, $pattern) {
  2875         4357  
  2875         4726  
  2875         3965  
575 2875 50       6861 croak 'assert_pattern called in void context' if not defined wantarray;
576 2875         5170 try {
577 2875     0   20877 local $SIG{__WARN__} = sub { die @_ };
  0         0  
578 2875         55121 qr/$pattern/;
579             }
580 3         9 catch ($e) { return E($state, $e); };
581 2872         18564 return 1;
582             }
583              
584             # this is only suitable for checking URIs within schemas themselves
585             # note that we cannot use $state->{specification_version} to more tightly constrain the plain-name
586             # fragment syntax, as we could be checking a $ref to a schema using a different version
587 5886     5886 0 8670 sub assert_uri_reference ($state, $schema) {
  5886         7191  
  5886         7293  
  5886         6391  
588 5886 50       10520 croak 'assert_uri_reference called in void context' if not defined wantarray;
589              
590 5886         14411 my $string = $schema->{$state->{keyword}};
591             return E($state, '%s value is not a valid URI-reference', $state->{keyword})
592             # see also uri-reference format sub
593 5886 100 66     21941 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      100        
      100        
594             or $string =~ /[^[:ascii:]]/ # ascii characters only
595             or $string =~ /#/ # no fragment, except...
596             and $string !~ m{#\z} # allow empty fragment
597             and $string !~ m{#[A-Za-z_][A-Za-z0-9_:.-]*\z} # allow plain-name fragment, superset of all drafts
598             and $string !~ m{#/(?:[^~]|~[01])*\z}; # allow json pointer fragment
599              
600 5852         1784442 return 1;
601             }
602              
603             # this is only suitable for checking URIs within schemas themselves,
604             # which have fragments consisting of plain names (anchors) or json pointers
605 6199     6199 0 9784 sub assert_uri ($state, $schema, $override = undef) {
  6199         8426  
  6199         7868  
  6199         9287  
  6199         7479  
606 6199 50       11945 croak 'assert_uri called in void context' if not defined wantarray;
607              
608 6199   33     12806 my $string = $override // $schema->{$state->{keyword}};
609 6199         22765 my $uri = Mojo::URL->new($string);
610              
611 6199 100 66     464828 return E($state, '"%s" is not a valid URI', $string)
      100        
      100        
      66        
      66        
      66        
612             # see also uri format sub
613             if fc($uri->to_unsafe_string) ne fc($string)
614             or $string =~ /[^[:ascii:]]/ # ascii characters only
615             or not $uri->is_abs # must have a scheme
616             or $string =~ /#/ # no fragment, except...
617             and $string !~ m{#\z} # empty fragment
618             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*\z} # plain-name fragment
619             and $string !~ m{#/(?:[^~]|~[01])*\z}; # json pointer fragment
620              
621 6183         1125201 return 1;
622             }
623              
624             # produces an annotation whose value is the same as that of the current schema keyword
625             # makes a copy as this is passed back to the user, who cannot be trusted to not mutate it
626 1138     1138 0 1367 sub annotate_self ($state, $schema) {
  1138         1426  
  1138         1419  
  1138         1329  
627             A($state, ref $schema->{$state->{keyword}} ? dclone($schema->{$state->{keyword}})
628 1138 100       11656 : $schema->{$state->{keyword}});
629             }
630              
631             # use original value as stored in the NV, without losing precision
632 1305     1305 0 2128 sub sprintf_num ($value) {
  1305         2000  
  1305         1742  
633 1305 100       2440 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
634             }
635              
636             {
637             # simple runtime-wide cache of $ids to filenames that are sourced from disk
638             my $schema_filename_cache = {};
639              
640             # adds a mapping from a URI to an absolute filename in the global runtime
641             # (available to all instances of the evaluator running in the same process).
642 945     945 0 990 sub register_schema ($uri, $filename) {
  945         1004  
  945         951  
  945         860  
643 945         2506 $schema_filename_cache->{$uri} = $filename;
644             }
645              
646 2000     2000 0 2857 sub get_schema_filename ($uri) {
  2000         2931  
  2000         2316  
647 2000         6928 $schema_filename_cache->{$uri};
648             }
649             }
650              
651             1;
652              
653             __END__