File Coverage

blib/lib/JSON/Schema/Modern/Utilities.pm
Criterion Covered Total %
statement 342 353 96.8
branch 160 182 87.9
condition 120 141 85.1
subroutine 57 60 95.0
pod 12 27 44.4
total 691 763 90.5


line stmt bran cond sub pod time code
1 45     45   351 use strict;
  45         108  
  45         1967  
2 45     45   272 use warnings;
  45         91  
  45         4242  
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.632';
8              
9 45     45   878 use 5.020;
  45         173  
10 45     45   251 use strictures 2;
  45         378  
  45         1962  
11 45     45   23394 use stable 0.031 'postderef';
  45         816  
  45         329  
12 45     45   8561 use experimental 'signatures';
  45         105  
  45         186  
13 45     45   3009 no autovivification warn => qw(fetch store exists delete);
  45         94  
  45         351  
14 45     45   3772 use if "$]" >= 5.022, experimental => 're_strict';
  45         102  
  45         1161  
15 45     45   3783 no if "$]" >= 5.031009, feature => 'indirect';
  45         100  
  45         3050  
16 45     45   275 no if "$]" >= 5.033001, feature => 'multidimensional';
  45         120  
  45         2722  
17 45     45   259 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  45         112  
  45         2839  
18 45     45   270 no if "$]" >= 5.041009, feature => 'smartmatch';
  45         85  
  45         2058  
19 45     45   279 no feature 'switch';
  45         88  
  45         1471  
20 45     45   243 use B;
  45         93  
  45         1356  
21 45     45   213 use Carp 'croak';
  45         82  
  45         3140  
22 45     45   298 use builtin::compat qw(blessed created_as_number);
  45         115  
  45         479  
23 45     45   10036 use Scalar::Util 'looks_like_number';
  45         98  
  45         2909  
24 45     45   305 use Storable 'dclone';
  45         84  
  45         2454  
25 45     45   314 use Feature::Compat::Try;
  45         87  
  45         496  
26 45     45   2971 use Mojo::JSON ();
  45         96  
  45         893  
27 45     45   224 use JSON::PP ();
  45         118  
  45         1283  
28 45     45   216 use Types::Standard qw(Str InstanceOf);
  45         106  
  45         487  
29 45     45   144242 use Mojo::File 'path';
  45         118  
  45         3003  
30 45     45   305 use namespace::clean;
  45         91  
  45         268  
31              
32 45     45   17151 use Exporter 'import';
  45         103  
  45         5565  
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             register_schema
62             load_cached_document
63             );
64              
65 45     45   360 use constant HAVE_BUILTIN => "$]" >= 5.035010;
  45         103  
  45         4335  
66 45     45   310 use if HAVE_BUILTIN, experimental => 'builtin';
  45         95  
  45         886  
67              
68 45     45   3280 use constant _BUILTIN_BOOLS => 0;
  45         114  
  45         8561  
69             use constant {
70             _BUILTIN_BOOLS && HAVE_BUILTIN && eval { +require Storable; Storable->VERSION(3.27); 1 }
71 45         292 && Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION(4.38); 1 }
72             ? (true => builtin::true, false => builtin::false)
73             : (true => JSON::PP::true, false => JSON::PP::false)
74 45     45   332 };
  45         98  
75              
76             # supports the six core types, plus integer (which is also a number)
77             # we do NOT check stringy_numbers here -- you must do that in the caller
78             # note that sometimes a value may return true for more than one type, e.g. integer+number,
79             # or number+string, depending on its internal flags.
80             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
81             # behaviour is consistent with get_type() (where integers are also numbers).
82 87601     87601 1 1718215 sub is_type ($type, $value, $config = {}) {
  87601         157573  
  87601         164348  
  87601         180598  
  87601         150034  
83 87601 100       228311 if ($type eq 'null') {
84 113         831 return !(defined $value);
85             }
86 87488 100       208067 if ($type eq 'boolean') {
87 11230         31695 return is_bool($value);
88             }
89 76258 100       176108 if ($type eq 'object') {
90 19889         127950 return ref $value eq 'HASH';
91             }
92 56369 100       134009 if ($type eq 'array') {
93 14092         95531 return ref $value eq 'ARRAY';
94             }
95              
96 42277 100 100     170383 if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
      100        
97 42261 100       102877 return 0 if not defined $value;
98 42241         282522 my $flags = B::svref_2object(\$value)->FLAGS;
99              
100             # dualvars with the same string and (stringified) numeric value could be either a string or a
101             # number, and before 5.36 we can't tell the difference, so we will answer yes for both.
102             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
103             # numified strings do have POK set, so we can tell which one came first.
104              
105 42241 100       136792 if ($type eq 'string') {
106             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
107             return !length ref($value)
108             && !(HAVE_BUILTIN && builtin::is_bool($value))
109             && $flags & B::SVf_POK
110             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
111 45   100 45   22591 || do { no warnings 'numeric'; 0+$value eq $value });
  45         144  
  45         34900  
  30274         414379  
112             }
113              
114 11967 100       38073 if ($type eq 'number') {
115             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
116 8048   100     24850 return is_bignum($value) || created_as_number($value);
117             }
118              
119 3919 50       14183 if ($type eq 'integer') {
120 3919 100       16198 if ($config->{legacy_ints}) {
121             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
122             # therefore 2.0 is NOT an integer
123 17   100     242 return ref($value) eq 'Math::BigInt'
124             || ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) && created_as_number($value);
125             }
126             else {
127             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
128             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
129             # if the exact type is important, or loss of any accuracy is unacceptable
130 3902   100     13036 return is_bignum($value) && $value->is_int
131             # if dualvar, PV and stringified NV/IV must be identical
132             || created_as_number($value) && int($value) == $value;
133             }
134             }
135             }
136              
137 16 100       106 if ($type =~ /^reference to (.+)\z/) {
138 11   33     173 return !blessed($value) && ref($value) eq $1;
139             }
140              
141 5         65 return ref($value) eq $type;
142             }
143              
144             # returns one of the six core types, plus integer
145             # we do NOT check stringy_numbers here -- you must do that in the caller
146             # pass { legacy_ints => 1 } in $config to use draft4 integer behaviour
147             # behaviour is consistent with is_type().
148 101644     101644 1 1243013 sub get_type ($value, $config = {}) {
  101644         177321  
  101644         209375  
  101644         160985  
149 101644 100       455265 return 'object' if ref $value eq 'HASH';
150 29807 100       73969 return 'boolean' if is_bool($value);
151 19270 100       72018 return 'null' if not defined $value;
152 18708 100       51970 return 'array' if ref $value eq 'ARRAY';
153              
154             # floats in json will always be parsed into Math::BigFloat, when allow_bignum is enabled
155 16343 100       41188 if (length ref $value) {
156 880         3243 my $ref = ref $value;
157             return $ref eq 'Math::BigInt' ? 'integer'
158 880 100 100     12460 : $ref eq 'Math::BigFloat' ? (!$config->{legacy_ints} && $value->is_int ? 'integer' : 'number')
    100          
    100          
    100          
159             : (defined blessed($value) ? '' : 'reference to ').$ref;
160             }
161              
162 15463         94201 my $flags = B::svref_2object(\$value)->FLAGS;
163              
164             # dualvars with the same string and (stringified) numeric value could be either a string or a
165             # number, and before 5.36 we can't tell the difference, so we choose number because it has been
166             # evaluated as a number already.
167             # in 5.36+, stringified numbers still get a PV but don't have POK set, whereas
168             # numified strings do have POK set, so we can tell which one came first.
169              
170             # like created_as_string, but rejects dualvars with stringwise-unequal string and numeric parts
171             return 'string'
172             if $flags & B::SVf_POK
173             && (!($flags & (B::SVf_IOK | B::SVf_NOK))
174 45 100 100 45   419 || do { no warnings 'numeric'; 0+$value eq $value });
  45   100     129  
  45         125110  
  15463         90116  
175              
176 6696 100       20939 if ($config->{legacy_ints}) {
177             # in draft4, an integer is "A JSON number without a fraction or exponent part.",
178             # therefore 2.0 is NOT an integer
179 316 100 66     2802 return ($flags & B::SVf_IOK) && !($flags & B::SVf_NOK) ? 'integer' : 'number'
    50          
180             if created_as_number($value);
181             }
182             else {
183             # note: values that are larger than $Config{ivsize} will be represented as an NV, not IV,
184             # therefore they will fail this check -- which is why use of Math::BigInt is recommended
185             # if the exact type is important, or loss of any accuracy is unacceptable
186 6380 100       46983 return int($value) == $value ? 'integer' : 'number' if created_as_number($value);
    100          
187             }
188              
189             # this might be a scalar with POK|IOK or POK|NOK set
190 15         68 return 'ambiguous type';
191             }
192              
193             # lifted from JSON::MaybeXS
194             # note: unlike builtin::compat::is_bool on older perls, we do not accept
195             # dualvar(0,"") or dualvar(1,"1") because JSON::PP and Cpanel::JSON::XS
196             # do not encode these as booleans.
197 65619     65619 1 115295 sub is_bool ($value) {
  65619         120629  
  65619         102543  
198 65619 100 66     981451 HAVE_BUILTIN and builtin::is_bool($value)
      66        
199             or
200             !!blessed($value)
201             and ($value->isa('JSON::PP::Boolean')
202             or $value->isa('Cpanel::JSON::XS::Boolean')
203             or $value->isa('JSON::XS::Boolean'));
204             }
205              
206 58928     58928 1 108065 sub is_schema ($value) {
  58928         108300  
  58928         93634  
207 58928 100       353599 ref $value eq 'HASH' || is_bool($value);
208             }
209              
210 13509     13509 0 24675 sub is_bignum ($value) {
  13509         26259  
  13509         21468  
211 13509         176721 ref($value) =~ /^Math::Big(?:Int|Float)\z/;
212             }
213              
214             # compares two arbitrary data payloads for equality, as per
215             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.2.2
216             # $state hashref supports the following fields:
217             # - scalarref_booleans (input): treats \0 and \1 as boolean values
218             # - stringy_numbers (input): strings will also be compared numerically
219             # - path (output): location of the first difference
220             # - error (output): description of the first difference
221 6454     6454 1 12808 sub is_equal ($x, $y, $state = {}) {
  6454         12990  
  6454         11825  
  6454         12092  
  6454         10739  
222 6454   100     38342 $state->{path} //= '';
223              
224 6454         20591 my @types = map get_type($_), $x, $y;
225              
226 6454 100       30886 $state->{error} = 'ambiguous type encountered', return 0
227             if grep $types[$_] eq 'ambiguous type', 0..1;
228              
229 6451 100       24468 if ($state->{scalarref_booleans}) {
230 88 100       282 ($x, $types[0]) = (0+!!$$x, 'boolean') if $types[0] eq 'reference to SCALAR';
231 88 100       298 ($y, $types[1]) = (0+!!$$y, 'boolean') if $types[1] eq 'reference to SCALAR';
232             }
233              
234 6451 100       21173 if ($state->{stringy_numbers}) {
235 16 100 100     139 ($x, $types[0]) = (0+$x, int(0+$x) == $x ? 'integer' : 'number')
    100          
236             if $types[0] eq 'string' and looks_like_number($x);
237              
238 16 100 100     103 ($y, $types[1]) = (0+$y, int(0+$y) == $y ? 'integer' : 'number')
    100          
239             if $types[1] eq 'string' and looks_like_number($y);
240             }
241              
242 6451 100       25893 $state->{error} = "wrong type: $types[0] vs $types[1]", return 0 if $types[0] ne $types[1];
243 5096 100       13755 return 1 if $types[0] eq 'null';
244 5076 100 100     30255 ($x eq $y and return 1), $state->{error} = 'strings not equal', return 0
245             if $types[0] eq 'string';
246 2475 100 100     18744 ($x == $y and return 1), $state->{error} = "$types[0]s not equal", return 0
247             if grep $types[0] eq $_, qw(boolean number integer);
248              
249 923         3961 my $path = $state->{path};
250 923 100       3596 if ($types[0] eq 'object') {
251 315 100       1950 $state->{error} = 'property count differs: '.keys(%$x).' vs '.keys(%$y), return 0
252             if keys %$x != keys %$y;
253              
254 292 100       2964 if (not is_equal(my $arr_x = [ sort keys %$x ], my $arr_y = [ sort keys %$y ], my $s={})) {
255 9         46 my $pos = substr($s->{path}, 1);
256 9         59 $state->{error} = 'property names differ starting at position '.$pos.' ("'.$arr_x->[$pos].'" vs "'.$arr_y->[$pos].'")';
257 9         85 return 0;
258             }
259              
260 283         1601 foreach my $property (sort keys %$x) {
261 333         1140 $state->{path} = jsonp($path, $property);
262 333 100       1545 return 0 if not is_equal($x->{$property}, $y->{$property}, $state);
263             }
264              
265 156         1971 return 1;
266             }
267              
268 608 50       2059 if ($types[0] eq 'array') {
269 608 100       2252 $state->{error} = 'element count differs: '.@$x.' vs '.@$y, return 0 if @$x != @$y;
270 597         2378 foreach my $idx (0 .. $x->$#*) {
271 653         2741 $state->{path} = $path.'/'.$idx;
272 653 100       4467 return 0 if not is_equal($x->[$idx], $y->[$idx], $state);
273             }
274 401         13768 return 1;
275             }
276              
277 0         0 $state->{error} = 'uh oh', return 0; # should never get here
278             }
279              
280             # checks array elements for uniqueness. short-circuits on first pair of matching elements
281             # $state hashref supports the following fields:
282             # - scalarref_booleans (input): treats \0 and \1 as boolean values
283             # - stringy_numbers (input): strings will also be compared numerically
284             # - path (output): location of the first difference
285             # - error (output): description of the first difference
286             # - equal_indices (output): the indices of identical items
287 3609     3609 1 10738 sub is_elements_unique ($array, $state = {}) {
  3609         7278  
  3609         7510  
  3609         6213  
288 3609         15722 foreach my $idx0 (0 .. $array->$#*-1) {
289 1467         6039 foreach my $idx1 ($idx0+1 .. $array->$#*) {
290 2123 100       10805 if (is_equal($array->[$idx0], $array->[$idx1], $state)) {
291 280 50       8021 push $state->{equal_indices}->@*, $idx0, $idx1 if exists $state->{equal_indices};
292 280         1688 return 0;
293             }
294             }
295             }
296 3329         17125 return 1;
297             }
298              
299             # shorthand for creating and appending json pointers
300             # the first argument is an already-encoded json pointer; remaining arguments are path segments to be
301             # encoded and appended
302             sub jsonp {
303 47930 50 66 47930 1 298907 warn q{first argument to jsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
304 47930         581864 return join('/', shift, map s!~!~0!gr =~ s!/!~1!gr, grep defined, @_);
305             }
306              
307             # splits a json pointer apart into its path segments
308             sub unjsonp {
309 0 0 0 0 1 0 warn q{argument to unjsonp should be '' or start with '/'} if length($_[0]) and substr($_[0],0,1) ne '/';
310 0         0 return map s!~0!~!gr =~ s!~1!/!gr, split m!/!, $_[0];
311             }
312              
313             # assigns a value to a data structure at a specific json pointer location
314             # operates destructively, in place, unless the root data or type is being modified
315 37     37 1 15663 sub jsonp_set ($data, $pointer, $value) {
  37         86  
  37         78  
  37         66  
  37         59  
316 37 100       195 if (not grep ref $data eq $_, qw(HASH ARRAY)) {
317 2 100       17 return $value if defined wantarray;
318 1         239 croak 'cannot write into non-reference in void context';
319             }
320              
321             # assigning to the root overwrites existing data
322 35 100       100 if (not length $pointer) {
323 6 100 100     65 if (ref $data eq 'HASH' and ref $value ne 'HASH'
      100        
      100        
324             or ref $data eq 'ARRAY' and ref $value ne 'ARRAY') {
325 4 100       20 return $value if defined wantarray;
326 2         334 croak 'cannot write into reference of different type in void context';
327             }
328              
329 2 100       11 $data->%* = $value->%* if ref $data eq 'HASH';
330 2 100       11 $data->@* = $value->@* if ref $data eq 'ARRAY';
331 2         16 return $data;
332             }
333              
334 29 50       237 my @keys = map +(s!~0!~!gr =~ s!~1!/!gr),
335             (length $pointer ? (split /\//, $pointer, -1) : ($pointer));
336              
337 29 100 66     486 croak 'cannot write hashref into a reference to an array in void context'
      100        
      66        
338             if @keys >= 2 and $keys[1] !~ /^\d+\z/a and ref $data eq 'ARRAY' and not defined wantarray;
339              
340 28         64 shift @keys; # always '', indicating the root
341 28         79 my $curp = \$data;
342              
343 28         67 foreach my $key (@keys) {
344             # if needed, first remove the existing data so we can replace with a new hash key or array index
345 61 100 100     297 undef $curp->$*
      100        
346             if not ref $curp->$*
347             or ref $curp->$* eq 'ARRAY' and $key !~ /^\d+\z/a;
348              
349             # use this existing hash key or array index location, or create new position
350 45     45   1107 use autovivification 'store';
  45         229  
  45         583  
351             $curp = \(
352             ref $curp->$* eq 'HASH' || $key !~ /^\d+\z/a
353 61 100 100     410 ? $curp->$*->{$key}
354             : $curp->$*->[$key]);
355             }
356              
357 28         76 $curp->$* = $value;
358 28         140 return $data;
359             }
360              
361             # returns a reusable Types::Standard type for json pointers
362             # TODO: move this off into its own distribution, see JSON::Schema::Types
363 360     360 1 147755 sub json_pointer_type () { Str->where('!length || m{^/} && !m{~(?![01])}'); }
  360         791  
  360         1613  
364              
365             # a URI without a fragment, or with a json pointer fragment
366 90     90 1 383614 sub canonical_uri_type () {
  90         240  
367 90         587 (InstanceOf['Mojo::URL'])->where(q{!defined($_->fragment) || $_->fragment =~ m{^/} && $_->fragment !~ m{~(?![01])}});
368             }
369              
370             # simple runtime-wide cache of $ids to schema document objects that are sourced from disk
371             {
372             my $document_cache = {};
373              
374             # Fetches a document from the cache (reading it from disk and creating the document if necessary),
375             # and add it to the evaluator.
376             # Normally this will just be a cache of schemas that are bundled with this distribution or a related
377             # distribution (such as OpenAPI-Modern), as duplicate identifiers are not checked for, unlike for
378             # normal schema additions.
379             # Only JSON-encoded files are supported at this time.
380 114     114 1 287 sub load_cached_document ($evaluator, $uri) {
  114         253  
  114         243  
  114         277  
381 114         373 $uri =~ s/#\z//; # older draft $ids use an empty fragment
382              
383             # see if it already exists as a document in the cache
384 114         20436 my $document = $document_cache->{$uri};
385              
386             # otherwise, load it from disk using our filename cache and create the document
387 114 100 100     20188 if (not $document and my $filename = get_schema_filename($uri)) {
388 68         13122 my $file = path($filename);
389 68 50       2583 die "uri $uri maps to file $file which does not exist" if not -f $file;
390 68         5454 my $schema = $evaluator->_json_decoder->decode($file->slurp);
391              
392             # avoid calling add_schema, which checksums the file to look for duplicates
393 68         18238 $document = JSON::Schema::Modern::Document->new(
394             schema => $schema,
395             evaluator => $evaluator,
396             skip_ref_checks => 1,
397             );
398              
399             # avoid calling add_document, which checks for duplicate identifiers (and would result in an
400             # infinite loop)
401 68 50       361 die JSON::Schema::Modern::Result->new(
402             output_format => $evaluator->output_format,
403             valid => 0,
404             errors => [ $document->errors ],
405             exception => 1,
406             ) if $document->has_errors;
407              
408 68         554 $document_cache->{$uri} = $document;
409             }
410              
411 114 100       19787 return if not $document;
412              
413             # bypass the normal collision checks, to avoid an infinite loop: these documents are presumed safe
414 103         543 $evaluator->_add_resources_unsafe(
415             map +($_->[0] => +{ $_->[1]->%*, document => $document }),
416             $document->resource_pairs
417             );
418              
419 103         65083 return $document;
420             }
421             }
422              
423             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
424              
425             # get all annotations produced for the current instance data location (that are visible to this
426             # schema location) - remember these are hashrefs, not Annotation objects
427 1395     1395 0 2916 sub local_annotations ($state) {
  1395         2781  
  1395         2477  
428 1395         10985 grep $_->{instance_location} eq $state->{data_path}, $state->{annotations}->@*;
429             }
430              
431             # shorthand for finding the current uri of the present schema location
432             # ensure that this code is kept consistent with the absolute_keyword_location builder in ResultNode
433             # Note that this may not be canonical if keyword_path has not yet been reset via the processing of a
434             # local identifier keyword (e.g. '$id').
435 34227     34227 0 69475 sub canonical_uri ($state, @extra_path) {
  34227         66481  
  34227         60821  
  34227         56929  
436 34227 100 66     257094 return $state->{initial_schema_uri} if not @extra_path and not length($state->{keyword_path});
437 12796         74719 my $uri = $state->{initial_schema_uri}->clone;
438 12796 50 100     1272844 my $fragment = ($uri->fragment//'').(@extra_path ? jsonp($state->{keyword_path}, @extra_path) : $state->{keyword_path});
439 12796 50       142556 undef $fragment if not length($fragment);
440 12796         40149 $uri->fragment($fragment);
441 12796         102570 $uri;
442             }
443              
444             # shorthand for creating error objects
445             # uses these keys from $state:
446             # - initial_schema_uri
447             # - keyword (optional)
448             # - data_path
449             # - traversed_keyword_path
450             # - keyword_path
451             # - _keyword_path_suffix (optional)
452             # - errors
453             # - exception (optional; set by abort())
454             # - recommended_response (optional)
455             # - depth
456             # - traverse (boolean, used for mode)
457             # returns defined-false, so callers can use 'return;' to differentiate between
458             # failed-with-no-error from failed-with-error.
459 13936     13936 0 75028 sub E ($state, $error_string, @args) {
  13936         27580  
  13936         27552  
  13936         32213  
  13936         23435  
460 13936 50       43206 croak 'E called in void context' if not defined wantarray;
461              
462             # sometimes the keyword shouldn't be at the very end of the schema path
463 13936         56389 my $sps = delete $state->{_keyword_path_suffix};
464 13936 100 100     90856 my @keyword_path_suffix = defined $sps && ref $sps eq 'ARRAY' ? $sps->@* : $sps//();
      100        
465              
466             # we store the absolute uri in unresolved form until needed,
467             # and perform the rest of the calculations later.
468 13936   100     95256 my $uri = [ $state->@{qw(initial_schema_uri keyword_path)}, $state->{keyword}//(), @keyword_path_suffix ];
469              
470             my $keyword_location = $state->{traversed_keyword_path}
471 13936         76571 .jsonp($state->@{qw(keyword_path keyword)}, @keyword_path_suffix);
472              
473 13936         125932 require JSON::Schema::Modern::Error;
474             push $state->{errors}->@*, JSON::Schema::Modern::Error->new(
475             depth => $state->{depth} // 0,
476             keyword => $state->{keyword},
477             $state->{traverse} ? () : (instance_location => $state->{data_path}),
478             keyword_location => $keyword_location,
479             # we calculate absolute_keyword_location when instantiating the Error object for Result
480             _uri => $uri,
481             error => @args ? sprintf($error_string, @args) : $error_string,
482             exception => $state->{exception},
483             ($state->%{recommended_response})x!!$state->{recommended_response},
484 13936 100 50     716637 mode => $state->{traverse} ? 'traverse' : 'evaluate',
    100          
    100          
485             );
486              
487 13936         128431 return 0;
488             }
489              
490             # shorthand for creating annotations
491             # uses these keys from $state:
492             # - initial_schema_uri
493             # - keyword (mandatory)
494             # - data_path
495             # - traversed_keyword_path
496             # - keyword_path
497             # - annotations
498             # - collect_annotations
499             # - _unknown (boolean)
500             # - depth
501 12008     12008 0 23348 sub A ($state, $annotation) {
  12008         24049  
  12008         24474  
  12008         21148  
502             # even if the user requested annotations, we only collect them for later drafts
503             # ..but we always collect them if the lowest bit is set, indicating the presence of unevaluated*
504             # keywords necessary for accurate validation
505             return 1 if not ($state->{collect_annotations}
506 12008 100       108391 & ($state->{specification_version} =~ /^draft[467]\z/ ? ~(1<<8) : ~0));
    100          
507              
508             # we store the absolute uri in unresolved form until needed,
509             # and perform the rest of the calculations later.
510 2638         13204 my $uri = [ $state->@{qw(initial_schema_uri keyword_path keyword)} ];
511              
512 2638         14204 my $keyword_location = $state->{traversed_keyword_path}.jsonp($state->@{qw(keyword_path keyword)});
513              
514             push $state->{annotations}->@*, {
515             depth => $state->{depth} // 0,
516             keyword => $state->{keyword},
517             instance_location => $state->{data_path},
518             keyword_location => $keyword_location,
519             # we calculate absolute_keyword_location when instantiating the Annotation object for Result
520             _uri => $uri,
521             annotation => $annotation,
522 2638 100 50     37856 $state->{_unknown} ? (unknown => 1) : (),
523             };
524              
525 2638         8609 return 1;
526             }
527              
528             # creates an error object, but also aborts evaluation immediately
529             # only this error is returned, because other errors on the stack might not actually be "real"
530             # errors (consider if we were in the middle of evaluating a "not" or "if").
531             # Therefore this is only appropriate during the evaluation phase, not the traverse phase.
532 34     34 0 402 sub abort ($state, $error_string, @args) {
  34         80  
  34         77  
  34         103  
  34         76  
533 34         492 ()= E({ %$state, exception => 1 }, $error_string, @args);
534 34 50       319 croak 'abort() called during traverse' if $state->{traverse};
535 34         569 die pop $state->{errors}->@*;
536             }
537              
538 0     0 0 0 sub assert_keyword_exists ($state, $schema) {
  0         0  
  0         0  
  0         0  
539 0 0       0 croak 'assert_keyword_exists called in void context' if not defined wantarray;
540 0 0       0 return E($state, '%s keyword is required', $state->{keyword}) if not exists $schema->{$state->{keyword}};
541 0         0 return 1;
542             }
543              
544 42311     42311 0 74184 sub assert_keyword_type ($state, $schema, $type) {
  42311         75428  
  42311         70513  
  42311         77877  
  42311         62609  
545 42311 50       105731 croak 'assert_keyword_type called in void context' if not defined wantarray;
546 42311 100       179320 return 1 if is_type($type, $schema->{$state->{keyword}});
547 17 100       157 E($state, '%s value is not a%s %s', $state->{keyword}, ($type =~ /^[aeiou]/ ? 'n' : ''), $type);
548             }
549              
550 2875     2875 0 7596 sub assert_pattern ($state, $pattern) {
  2875         6249  
  2875         6551  
  2875         5161  
551 2875 50       9667 croak 'assert_pattern called in void context' if not defined wantarray;
552 2875         7590 try {
553 2875     0   29123 local $SIG{__WARN__} = sub { die @_ };
  0         0  
554 2875         53794 qr/$pattern/;
555             }
556 3         17 catch ($e) { return E($state, $e); };
557 2872         29269 return 1;
558             }
559              
560             # this is only suitable for checking URIs within schemas themselves
561             # note that we cannot use $state->{specification_version} to more tightly constrain the plain-name
562             # fragment syntax, as we could be checking a $ref to a schema using a different version
563 5886     5886 0 11977 sub assert_uri_reference ($state, $schema) {
  5886         10892  
  5886         9898  
  5886         9625  
564 5886 50       15692 croak 'assert_uri_reference called in void context' if not defined wantarray;
565              
566 5886         22995 my $string = $schema->{$state->{keyword}};
567             return E($state, '%s value is not a valid URI-reference', $state->{keyword})
568             # see also uri-reference format sub
569 5886 100 66     30911 if fc(Mojo::URL->new($string)->to_unsafe_string) ne fc($string)
      100        
      100        
      100        
      100        
570             or $string =~ /[^[:ascii:]]/ # ascii characters only
571             or $string =~ /#/ # no fragment, except...
572             and $string !~ m{#\z} # allow empty fragment
573             and $string !~ m{#[A-Za-z_][A-Za-z0-9_:.-]*\z} # allow plain-name fragment, superset of all drafts
574             and $string !~ m{#/(?:[^~]|~[01])*\z}; # allow json pointer fragment
575              
576 5852         2656781 return 1;
577             }
578              
579             # this is only suitable for checking URIs within schemas themselves,
580             # which have fragments consisting of plain names (anchors) or json pointers
581 6199     6199 0 15928 sub assert_uri ($state, $schema, $override = undef) {
  6199         11858  
  6199         12188  
  6199         13979  
  6199         10713  
582 6199 50       22359 croak 'assert_uri called in void context' if not defined wantarray;
583              
584 6199   33     18894 my $string = $override // $schema->{$state->{keyword}};
585 6199         35816 my $uri = Mojo::URL->new($string);
586              
587 6199 100 66     678789 return E($state, '"%s" is not a valid URI', $string)
      100        
      100        
      66        
      66        
      66        
588             # see also uri format sub
589             if fc($uri->to_unsafe_string) ne fc($string)
590             or $string =~ /[^[:ascii:]]/ # ascii characters only
591             or not $uri->is_abs # must have a scheme
592             or $string =~ /#/ # no fragment, except...
593             and $string !~ m{#\z} # empty fragment
594             and $string !~ m{#[A-Za-z][A-Za-z0-9_:.-]*\z} # plain-name fragment
595             and $string !~ m{#/(?:[^~]|~[01])*\z}; # json pointer fragment
596              
597 6183         1720805 return 1;
598             }
599              
600             # produces an annotation whose value is the same as that of the current schema keyword
601             # makes a copy as this is passed back to the user, who cannot be trusted to not mutate it
602 1138     1138 0 2098 sub annotate_self ($state, $schema) {
  1138         1893  
  1138         1952  
  1138         1924  
603             A($state, ref $schema->{$state->{keyword}} ? dclone($schema->{$state->{keyword}})
604 1138 100       65540 : $schema->{$state->{keyword}});
605             }
606              
607             # use original value as stored in the NV, without losing precision
608 1305     1305 0 3500 sub sprintf_num ($value) {
  1305         3269  
  1305         2388  
609 1305 100       3553 is_bignum($value) ? $value->bstr : sprintf('%s', $value);
610             }
611              
612             {
613             # simple runtime-wide cache of $ids to filenames that are sourced from disk
614             my $schema_filename_cache = {};
615              
616             # adds a mapping from a URI to an absolute filename in the global runtime
617             # (available to all instances of the evaluator running in the same process).
618 945     945 0 1472 sub register_schema ($uri, $filename) {
  945         1548  
  945         1352  
  945         1285  
619 945         3836 $schema_filename_cache->{$uri} = $filename;
620             }
621              
622 2000     2000 0 3965 sub get_schema_filename ($uri) {
  2000         3931  
  2000         3309  
623 2000         10145 $schema_filename_cache->{$uri};
624             }
625             }
626              
627             1;
628              
629             __END__