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   286 use strict;
  45         77  
  45         1459  
2 45     45   164 use warnings;
  45         57  
  45         2935  
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.635';
8              
9 45     45   636 use 5.020;
  45         144  
10 45     45   172 use strictures 2;
  45         264  
  45         1229  
11 45     45   14831 use stable 0.031 'postderef';
  45         501  
  45         196  
12 45     45   5472 use experimental 'signatures';
  45         70  
  45         164  
13 45     45   1860 no autovivification warn => qw(fetch store exists delete);
  45         71  
  45         222  
14 45     45   2777 use if "$]" >= 5.022, experimental => 're_strict';
  45         67  
  45         763  
15 45     45   2500 no if "$]" >= 5.031009, feature => 'indirect';
  45         72  
  45         2079  
16 45     45   205 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         66  
  45         1687  
17 45     45   157 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         64  
  45         1751  
18 45     45   181 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         68  
  45         1272  
19 45     45   145 no feature 'switch';
  45         79  
  45         888  
20 45     45   166 use B;
  45         60  
  45         929  
21 45     45   129 use Carp qw(carp croak);
  45         63  
  45         2372  
22 45     45   203 use builtin::compat qw(blessed created_as_number);
  45         59  
  45         432  
23 45     45   6613 use Scalar::Util 'looks_like_number';
  45         99  
  45         1795  
24 45     45   201 use Storable 'dclone';
  45         89  
  45         1445  
25 45     45   184 use Feature::Compat::Try;
  45         57  
  45         359  
26 45     45   1840 use Mojo::JSON ();
  45         65  
  45         504  
27 45     45   157 use Mojo::JSON::Pointer ();
  45         91  
  45         602  
28 45     45   207 use JSON::PP ();
  45         68  
  45         793  
29 45     45   175 use Types::Standard qw(Str InstanceOf Enum);
  45         76  
  45         317  
30 45     45   103955 use Mojo::File 'path';
  45         95  
  45         2298  
31 45     45   199 use namespace::clean;
  45         70  
  45         203  
32              
33 45     45   11310 use Exporter 'import';
  45         73  
  45         3777  
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   206 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  45         67  
  45         3240  
70 45     45   220 use if HAVE_BUILTIN, experimental => 'builtin';
  45         60  
  45         689  
71              
72 45     45   2156 use constant _BUILTIN_BOOLS => 0;
  45         72  
  45         6119  
73             use constant {
74             _BUILTIN_BOOLS && HAVE_BUILTIN && eval { +require Storable; Storable->VERSION(3.27); 1 }
75 45         212 && 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   209 };
  45         65  
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 975087 sub is_type ($type, $value, $config = {}) {
  87648         101895  
  87648         107240  
  87648         111724  
  87648         97093  
87 87648 100       134417 if ($type eq 'null') {
88 113         518 return !(defined $value);
89             }
90 87535 100       129035 if ($type eq 'boolean') {
91 11231         20690 return is_bool($value);
92             }
93 76304 100       121418 if ($type eq 'object') {
94 19901         75583 return ref $value eq 'HASH';
95             }
96 56403 100       87511 if ($type eq 'array') {
97 14093         56289 return ref $value eq 'ARRAY';
98             }
99              
100 42310 100 100     108611 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
101 42294 100       63805 return 0 if not defined $value;
102 42274         187921 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       84425 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   13755 || do { no warnings 'numeric'; 0+$value eq $value });
  45         100  
  45         23695  
  30307         254885  
116             }
117              
118 11967 100       24176 if ($type eq 'number') {
119             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
120 8048   100     14826 return is_bignum($value) || created_as_number($value);
121             }
122              
123 3919 50       7955 if ($type eq 'integer') {
124 3919 100       9132 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     125 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     7484 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       77 if ($type =~ /^reference to (.+)\z/) {
142 11   33     129 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 122332     122332 1 742726 sub get_type ($value, $config = {}) {
  122332         138241  
  122332         149905  
  122332         127453  
153 122332 100       305001 return 'object' if ref $value eq 'HASH';
154 47736 100       67648 return 'boolean' if is_bool($value);
155 36336 100       68266 return 'null' if not defined $value;
156 35750 100       56138 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 29879 100       48998 if (length(my $ref = ref $value)) {
160             return $ref eq 'Math::BigInt' ? 'integer'
161 880 100 100     5346 : $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 28999         80212 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   827 || do { no warnings 'numeric'; 0+$value eq $value });
  45   100     223  
  45         84906  
  28999         93061  
178              
179 6910 100       13713 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     1910 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 6594 100       29235 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         57 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 83566     83566 1 91840 sub is_bool ($value) {
  83566         97606  
  83566         84003  
201 83566 100 66     645138 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 73781 sub is_schema ($value) {
  58973         71671  
  58973         61695  
210 58973 100       200694 ref $value eq 'HASH' || is_bool($value);
211             }
212              
213 13509     13509 0 15827 sub is_bignum ($value) {
  13509         18787  
  13509         14677  
214 13509         102137 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 16767     16767 1 135030 sub is_equal ($x, $y, $state = {}) {
  16767         19472  
  16767         19104  
  16767         18158  
  16767         16838  
225 16767   100     41126 $state->{path} //= '';
226              
227 16767         26608 my @types = map get_type($_), $x, $y;
228              
229 16767 100       37140 $state->{error} = 'ambiguous type encountered', return 0
230             if grep $types[$_] eq 'ambiguous type', 0..1;
231              
232 16764 100       29358 if ($state->{scalarref_booleans}) {
233 88 100       185 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
234 88 100       164 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
235             }
236              
237 16764 100       26516 if ($state->{stringy_numbers}) {
238 16 100 100     96 ($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     70 ($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 16764 100       29438 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
246 15409 100       21340 return 1 if $types[0] eq 'null';
247 15377 100 100     39227 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
248             if $types[0] eq 'string';
249 6125 100 100     19489 ($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 4034         6428 my $path = $state->{path};
253 4034 100       6616 if ($types[0] eq 'object') {
254 1673 100       4444 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
255             if keys %$x != keys %$y;
256              
257 1650 100       9691 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
258 9         28 my $pos = substr($s->{path}, 1);
259 9         39 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
260 9         52 return 0;
261             }
262              
263 1641         5035 foreach my $property (sort keys %$x) {
264 3938         5641 $state->{path} = jsonp($path, $property);
265 3938 100       7570 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
266             }
267              
268 1514         8537 return 1;
269             }
270              
271 2361 50       4084 if ($types[0] eq 'array') {
272 2361 100       4294 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
273 2350         5010 foreach my $idx (0 .. $x->$#*) {
274 5459         9477 $state->{path} = $path.'/'.$idx;
275 5459 100       10066 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
276             }
277 2154         12934 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 5136 sub is_elements_unique ($array, $state = {}) {
  3609         4463  
  3609         4724  
  3609         4455  
291 3609         10234 foreach my $idx0 (0 .. $array->$#*-1) {
292 1467         3987 foreach my $idx1 ($idx0+1 .. $array->$#*) {
293 2123 100       5899 if (is_equal($array->[$idx0], $array->[$idx1], $state)) {
294 280 50       4151 push $state->{equal_indices}->@*, $idx0, $idx1 if exists $state->{equal_indices};
295 280         1015 return 0;
296             }
297             }
298             }
299 3329         10475 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 204628 carp q{first argument to jsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
307 51580         386812 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 43718 sub jsonp_set ($data, $pointer, $value) {
  17470         28363  
  17470         25564  
  17470         24339  
  17470         20423  
323 17470 100 100     94778 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       38966 if (not length $pointer) {
328 17412 100 100     45244 if (not ref $data or ref $data ne ref $value) {
329 17410 100       79591 return $value if defined wantarray;
330 2         216 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       3 $data = [] if not ref $data;
339 1         3 $data->@* = $value->@*;
340             }
341              
342 2         5 return $data;
343             }
344              
345 56 50       324 my @keys = map +(s!~0!~!gr =~ s!~1!/!gr),
346             (length $pointer ? (split /\//, $pointer, -1) : ($pointer));
347              
348 56 100 66     517 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         102 shift @keys; # always '', indicating the root
352 55         73 my $curp = \$data;
353              
354 55         92 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     291 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   335 use autovivification 'store';
  45         110  
  45         268  
362             $curp = \(
363             ref $curp->$* eq 'HASH' || $key !~ /^(?:\d+|-)\z/a
364 113 100 100     428 ? $curp->$*->{$key}
    100          
365             : $key =~ /^\d+\z/a
366             ? $curp->$*->[$key]
367             : $curp->$*->[$curp->$*->$#* + 1]);
368             }
369              
370 55         80 $curp->$* = $value;
371 55         253 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 98237 sub json_pointer_type () { Str->where('!length || m{^/} && !m{~(?![01])}'); }
  360         552  
  360         1200  
377              
378             # a URI without a fragment, or with a json pointer fragment
379 90     90 1 249875 sub canonical_uri_type () {
  90         171  
380 90         449 (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 119162 sub core_types_type () {
  90         136  
387 90         393 Enum[qw(null object array boolean string number)];
388             }
389              
390 4     4 1 21 sub core_formats_type () {
  4         6  
391 4         24 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 200 sub load_cached_document ($evaluator, $uri) {
  114         186  
  114         186  
  114         171  
405 114         236 $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         12198 my $document = $document_cache->{$uri};
409              
410             # otherwise, load it from disk using our filename cache and create the document
411 114 100 100     12021 if (not $document and my $filename = get_schema_filename($uri)) {
412 68         7343 my $file = path($filename);
413 68 50       1682 die "uri $uri maps to file $file which does not exist" if not -f $file;
414 68         4732 my $schema = $evaluator->_json_decoder->decode($file->slurp);
415              
416             # avoid calling add_schema, which checksums the file to look for duplicates
417 68         13335 $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       273 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         408 $document_cache->{$uri} = $document;
433             }
434              
435 114 100       13337 return if not $document;
436              
437             # bypass the normal collision checks, to avoid an infinite loop: these documents are presumed safe
438 103         363 $evaluator->_add_resources_unsafe(
439             map +($_->[0] => +{ $_->[1]->%*, document => $document }),
440             $document->resource_pairs
441             );
442              
443 103         43957 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 2003 sub local_annotations ($state) {
  1395         1697  
  1395         1946  
452 1395         6277 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 43002 sub canonical_uri ($state, @extra_path) {
  34245         41662  
  34245         38116  
  34245         38783  
460 34245 100 66     152190 return $state->{initial_schema_uri} if not @extra_path and not length($state->{keyword_path});
461 12799         46741 my $uri = $state->{initial_schema_uri}->clone;
462 12799 50 100     833882 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{keyword_path}, @extra_path) : $state->{keyword_path});
463 12799 50       91721 undef $fragment if not length($fragment);
464 12799         26767 $uri->fragment($fragment);
465 12799         62640 $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 49647 sub E ($state, $error_string, @args) {
  13954         18031  
  13954         19177  
  13954         20163  
  13954         16793  
484 13954 50       26674 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         32856 my $sps = delete $state->{_keyword_path_suffix};
488 13954 100 100     58008 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     61445 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         44906 .jsonp($state->@{qw(keyword_path keyword)}, @keyword_path_suffix);
496              
497 13954         83639 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     436783 mode => $state->{traverse} ? 'traverse' : 'evaluate',
    100          
    100          
509             );
510              
511 13954         78744 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 15490 sub A ($state, $annotation) {
  12028         14784  
  12028         15352  
  12028         12838  
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       67313 & ($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         8075 my $uri = [ $state->@{qw(initial_schema_uri keyword_path keyword)} ];
535              
536 2638         8210 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     22395 $state->{_unknown} ? (unknown => 1) : (),
547             };
548              
549 2638         5297 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 375 sub abort ($state, $error_string, @args) {
  34         52  
  34         56  
  34         64  
  34         48  
557 34         654 ()= E({ %$state, exception => 1 }, $error_string, @args);
558 34 50       240 croak 'abort() called during traverse' if $state->{traverse};
559 34         448 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 50262 sub assert_keyword_type ($state, $schema, $type) {
  42348         47805  
  42348         49113  
  42348         50559  
  42348         44082  
569 42348 50       70247 croak 'assert_keyword_type called in void context' if not defined wantarray;
570 42348 100       106878 return 1 if is_type($type, $schema->{$state->{keyword}});
571 17 100       102 E($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
572             }
573              
574 2875     2875 0 4315 sub assert_pattern ($state, $pattern) {
  2875         3948  
  2875         4167  
  2875         3645  
575 2875 50       6099 croak 'assert_pattern called in void context' if not defined wantarray;
576 2875         4935 try {
577 2875     0   19721 local $SIG{__WARN__} = sub { die @_ };
  0         0  
578 2875         44599 qr/$pattern/;
579             }
580 3         9 catch ($e) { return E($state, $e); };
581 2872         17730 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 8332 sub assert_uri_reference ($state, $schema) {
  5886         7285  
  5886         6785  
  5886         6386  
588 5886 50       10292 croak 'assert_uri_reference called in void context' if not defined wantarray;
589              
590 5886         14025 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     21321 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         1749937 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 8492 sub assert_uri ($state, $schema, $override = undef) {
  6199         9589  
  6199         8655  
  6199         8531  
  6199         6812  
606 6199 50       15768 croak 'assert_uri called in void context' if not defined wantarray;
607              
608 6199   33     11823 my $string = $override // $schema->{$state->{keyword}};
609 6199         20422 my $uri = Mojo::URL->new($string);
610              
611 6199 100 66     437463 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         1055895 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 1347 sub annotate_self ($state, $schema) {
  1138         1361  
  1138         1391  
  1138         1249  
627             A($state, ref $schema->{$state->{keyword}} ? dclone($schema->{$state->{keyword}})
628 1138 100       10744 : $schema->{$state->{keyword}});
629             }
630              
631             # use original value as stored in the NV, without losing precision
632 1305     1305 0 2009 sub sprintf_num ($value) {
  1305         1899  
  1305         1737  
633 1305 100       2424 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 969 sub register_schema ($uri, $filename) {
  945         969  
  945         962  
  945         872  
643 945         2440 $schema_filename_cache->{$uri} = $filename;
644             }
645              
646 2000     2000 0 2681 sub get_schema_filename ($uri) {
  2000         2733  
  2000         2432  
647 2000         6458 $schema_filename_cache->{$uri};
648             }
649             }
650              
651             1;
652              
653             __END__