File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 1023 1037 98.6
branch 266 320 83.1
condition 160 184 86.9
subroutine 229 229 100.0
pod 14 17 82.3
total 1692 1787 94.6


line stmt bran cond sub pod time code
1 51     51   13977494 use strict;
  51         90  
  51         1702  
2 51     51   221 use warnings;
  51         85  
  51         3846  
3             package JSON::Schema::Modern; # git description: v0.640-7-g1b0501d9
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Validate data against a schema using a JSON Schema
6             # KEYWORDS: JSON Schema validator data validation structure specification
7              
8             our $VERSION = '0.641';
9              
10 51     51   767 use 5.020; # for fc, unicode_strings features
  51         193  
11 51     51   22988 use Moo;
  51         292358  
  51         193  
12 51     51   58109 use strictures 2;
  51         379  
  51         1715  
13 51     51   15957 use stable 0.031 'postderef';
  51         663  
  51         309  
14 51     51   7986 use experimental 'signatures';
  51         123  
  51         230  
15 51     51   2265 no autovivification warn => qw(fetch store exists delete);
  51         141  
  51         334  
16 51     50   3125 use if "$]" >= 5.022, experimental => 're_strict';
  50         80  
  50         1174  
17 50     50   3080 no if "$]" >= 5.031009, feature => 'indirect';
  50         93  
  50         2603  
18 50     50   256 no if "$]" >= 5.033001, feature => 'multidimensional';
  50         91  
  50         2150  
19 50     50   239 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  50         94  
  50         2055  
20 50     50   221 no if "$]" >= 5.041009, feature => 'smartmatch';
  50         84  
  50         1598  
21 50     50   363 no feature 'switch';
  50         100  
  50         1157  
22 50     50   21034 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  50         7512028  
  50         1826  
23 50     50   459 use Carp qw(croak carp);
  50         227  
  50         3161  
24 50     49   276 use List::Util 1.55 qw(pairs first uniqint pairmap uniqstr min);
  49         1139  
  48         3965  
25 48     49   552 use if "$]" < 5.041010, 'List::Util' => 'any';
  49         648  
  49         1925  
26 49     49   211 use if "$]" >= 5.041010, experimental => 'keyword_any';
  49         506  
  48         865  
27 48     49   23062 use builtin::compat qw(refaddr load_module blessed);
  49         669496  
  49         3625  
28 49     49   27653 use Mojo::URL;
  49         370597  
  49         2911  
29 49     49   21630 use Mojo::File 'path';
  49         696657  
  49         5636  
30 49     49   19470 use Clone 'clone';
  49         20226  
  49         5120  
31 49     48   312 use File::ShareDir 'dist_dir';
  48         75  
  48         2737  
32 48     48   18325 use MooX::TypeTiny 0.002002;
  48         15251  
  48         223  
33 48     48   305668 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  48         4568071  
  48         575  
34 48     48   173885 use Digest::MD5 'md5';
  48         103  
  48         2952  
35 48     48   23771 use Feature::Compat::Try;
  48         15023  
  48         187  
36 48     48   22236 use JSON::Schema::Modern::Error;
  48         929  
  48         1873  
37 48     48   22659 use JSON::Schema::Modern::Result;
  48         528  
  48         1754  
38 48     48   23438 use JSON::Schema::Modern::Document;
  48         893  
  48         833  
39 48     48   2951 use JSON::Schema::Modern::Utilities qw(get_type canonical_uri E abort annotate_self jsonp is_type assert_uri local_annotations is_schema json_pointer_type canonical_uri_type core_types_type core_formats_type load_cached_document jsonp_set);
  48         97  
  48         5076  
40 48     48   269 use namespace::clean;
  48         80  
  48         426  
41              
42             our @CARP_NOT = qw(
43             JSON::Schema::Modern::Document
44             JSON::Schema::Modern::Vocabulary
45             JSON::Schema::Modern::Vocabulary::Applicator
46             JSON::Schema::Modern::Document::OpenAPI
47             OpenAPI::Modern
48             );
49              
50 48     48   27667 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  48         90  
  48         3253  
51 48     48   272 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  48         83  
  48         32260  
52              
53             has specification_version => (
54             is => 'ro',
55             isa => Enum(SPECIFICATION_VERSIONS_SUPPORTED),
56             coerce => sub {
57             return $_[0] if any { $_[0] eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*;
58             my $real = 'draft'.($_[0]//'');
59             (any { $real eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*) ? $real : $_[0];
60             },
61             );
62              
63             has output_format => (
64             is => 'ro',
65             isa => Enum(JSON::Schema::Modern::Result->OUTPUT_FORMATS),
66             default => 'basic',
67             );
68              
69             has short_circuit => (
70             is => 'ro',
71             isa => Bool,
72             lazy => 1,
73             default => sub { $_[0]->output_format eq 'flag' && !$_[0]->collect_annotations },
74             );
75              
76             has max_depth => (
77             is => 'ro',
78             isa => Int,
79             default => 50,
80             );
81              
82 1     1 0 8 sub max_traversal_depth ($self) {
  1         3  
  1         1  
83 1         13 carp 'max_traversal_depth is deprecated and could be removed anytime after 2026-05-09; use max_depth instead';
84 1         3 $self->max_depth;
85             }
86              
87             has validate_formats => (
88             is => 'ro',
89             isa => Bool,
90             lazy => 1,
91             # as specified by https://json-schema.org/draft//schema#/$vocabulary
92             default => sub { ($_[0]->specification_version//SPECIFICATION_VERSION_DEFAULT) =~ /^draft[467]\z/ ? 1 : 0 },
93             );
94              
95             has validate_content_schemas => (
96             is => 'ro',
97             isa => Bool,
98             lazy => 1,
99             # defaults to false in latest versions, as specified by
100             # https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.2
101             default => sub { ($_[0]->specification_version//'') eq 'draft7' },
102             );
103              
104             has [qw(collect_annotations scalarref_booleans stringy_numbers strict with_defaults)] => (
105             is => 'ro',
106             isa => Bool,
107             );
108              
109              
110             # { $format_name => { type => ..., sub => ... }, ... }
111             has _format_validations => (
112             is => 'bare',
113             isa => my $format_type = HashRef[Dict[
114             type => core_types_type|ArrayRef[core_types_type],
115             sub => CodeRef,
116             ]],
117             init_arg => 'format_validations',
118             );
119              
120 969   100 969   1187 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  969         1109  
  969         1320  
  969         1001  
  969         6813  
121              
122 12     12 1 34862 sub add_format_validation ($self, $format, $definition) {
  12         25  
  12         23  
  12         23  
  12         21  
123 12 50 100     98 return if exists(($self->{_format_validations}//{})->{$format});
124              
125 12 100       53 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
126 12         97 $format_type->({ $format => $definition });
127              
128             # all core formats are of type string (so far); changing type of custom format is permitted
129             croak "Type for override of format $format does not match original type"
130 5 100 100     387 if core_formats_type->check($format) and $definition->{type} ne 'string';
131              
132 48     48   365 use autovivification 'store';
  48         124  
  48         461  
133 4         3084 $self->{_format_validations}{$format} = $definition;
134             }
135              
136             around BUILDARGS => sub ($orig, $class, @args) {
137             my $args = $class->$orig(@args);
138             croak 'output_format: strict_basic can only be used with specification_version: draft2019-09'
139             if ($args->{output_format}//'') eq 'strict_basic'
140             and ($args->{specification_version}//'') ne 'draft2019-09';
141              
142             croak 'collect_annotations cannot be used with specification_version '.$args->{specification_version}
143             if $args->{collect_annotations} and ($args->{specification_version}//'') =~ /^draft[467]\z/;
144              
145             $args->{max_depth} = delete $args->{max_traversal_depth} if exists $args->{max_traversal_depth};
146              
147             $args->{format_validations} = +{
148             map +($_->[0] => ref $_->[1] eq 'HASH' ? $_->[1] : +{ type => 'string', sub => $_->[1] }),
149             pairs $args->{format_validations}->%*
150             } if $args->{format_validations};
151              
152             return $args;
153             };
154              
155             my $_isa = sub ($obj, $class) { blessed($obj) && $obj->isa($class) };
156              
157             sub add_schema {
158 17395 50   17395 1 85896 croak 'insufficient arguments' if @_ < 2;
159 17395         25986 my $self = shift;
160              
161 17395 100       47936 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
162 2         184 Carp::carp('use of deprecated form of add_schema with document');
163 2         29 return $self->add_document($_[0]);
164             }
165              
166             # TODO: resolve $uri against $self->base_uri
167 17394 50       37618 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
168             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
169              
170 17394 100       133562 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
171 17393 50       87283 croak 'insufficient arguments' if not @_;
172              
173 17393 100       31637 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
174 2         296 Carp::carp('use of deprecated form of add_schema with document');
175 2         21 return $self->add_document($uri, $_[0]);
176             }
177              
178             # document BUILD will trigger $self->traverse($schema)
179             # Note we do not pass the uri to the document constructor, so resources in that document may still
180             # be relative
181 17392         364965 my $document = JSON::Schema::Modern::Document->new(
182             schema => $_[0],
183             evaluator => $self, # used mainly for traversal during document construction
184             );
185              
186             # try to reuse the same document, if the same schema is being added twice:
187             # this results in _add_resource silently ignoring the duplicate add, rather than erroring.
188 17392         276054 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
189 17392 100       822174 if (my $existing_doc = first {
190 718009   66 718009   7210748 my $existing_checksum = $_->_checksum
191             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
192 718009 100       3192327 $existing_checksum eq $schema_checksum
193             and $_->canonical_uri eq $document->canonical_uri
194             # FIXME: must also check spec version/metaschema_uri/vocabularies
195             } uniqint map $_->{document}, $self->_canonical_resources) {
196 12303         2576666 $document = $existing_doc;
197             }
198              
199 17392         457752 $self->add_document($uri, $document);
200             }
201              
202             sub add_document {
203 18204 50   18204 1 97750 croak 'insufficient arguments' if @_ < 2;
204 18204         31234 my $self = shift;
205              
206             # TODO: resolve $uri against $self->base_uri
207 18204 50       53199 my $base_uri = !$_[0]->$_isa('JSON::Schema::Modern::Document') ? Mojo::URL->new(shift)
    100          
208             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
209              
210 18204 50       4528602 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
211 18204 50       84163 croak 'insufficient arguments' if not @_;
212              
213 18204         27601 my $document = shift;
214 18204 50       39449 croak 'wrong document type' if not $document->$_isa('JSON::Schema::Modern::Document');
215              
216             # we will never add a document to the resource index if it has errors
217 18204 100       61070 die JSON::Schema::Modern::Result->new(
218             output_format => $self->output_format,
219             valid => 0,
220             errors => [ $document->errors ],
221             exception => 1,
222             ) if $document->has_errors;
223              
224 18024 100       45612 if (not length $base_uri){
225 17191         1528101 foreach my $res_pair ($document->resource_pairs) {
226 18052         195212 my ($uri_string, $doc_resource) = @$res_pair;
227              
228             # this might croak if there are duplicates or malformed entries.
229 18052         126377 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
230             }
231              
232 17186         5512782 return $document;
233             }
234              
235 834         118022 my @root; # uri_string => resource hash of the resource at path ''
236              
237             # document resources are added after resolving each resource against our provided base uri
238 834         2278 foreach my $res_pair ($document->resource_pairs) {
239 864         1725 my ($uri_string, $doc_resource) = @$res_pair;
240 864         1967 $uri_string = Mojo::URL->new($uri_string)->to_abs($base_uri)->to_string;
241              
242             my $new_resource = {
243             canonical_uri => Mojo::URL->new($doc_resource->{canonical_uri})->to_abs($base_uri),
244 864         365513 $doc_resource->%{qw(path specification_version vocabularies)},
245             document => $document,
246             };
247              
248 864   100     299052 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
249 48     48   54484 use autovivification 'store';
  48         96  
  48         203  
250             $new_resource->{anchors}{$anchor} = {
251             $doc_resource->{anchors}{$anchor}->%{path},
252             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
253 170 100       1532 canonical_uri => Mojo::URL->new($doc_resource->{anchors}{$anchor}{canonical_uri})->to_abs($base_uri),
254             };
255             }
256              
257             # this might croak if there are duplicates or malformed entries.
258 864         58220 $self->_add_resource($uri_string => $new_resource);
259 857 100 66     286681 @root = ($uri_string => $new_resource) if $new_resource->{path} eq '' and $uri_string !~ /#./;
260             }
261              
262             # associate the root resource with the base uri we were provided, if it does not already exist
263 827 100       3163 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
264              
265 827         126442 return $document;
266             }
267              
268 4     4 1 2729 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         30  
  4         10  
  4         7  
  4         14  
  4         9  
269 4 50       13 croak 'evaluate_json_string called in void context' if not defined wantarray;
270              
271 4         13 my $data;
272 4         7 try {
273 4         77 $data = $self->_json_decoder->decode($json_data)
274             }
275             catch ($e) {
276 3         79 return JSON::Schema::Modern::Result->new(
277             output_format => $self->output_format,
278             valid => 0,
279             exception => 1,
280             errors => [
281             JSON::Schema::Modern::Error->new(
282             depth => 0,
283             mode => 'traverse',
284             keyword => undef,
285             keyword_location => '',
286             error => $e,
287             )
288             ],
289             );
290             }
291              
292 2         17 return $self->evaluate($data, $schema, $config_override);
293             }
294              
295             # this is called whenever we need to walk a document for something.
296             # for now it is just called when a ::Document object is created, to verify the integrity of the
297             # schema structure, to identify the metaschema (via the $schema keyword), and to extract all
298             # embedded resources via $id and $anchor keywords within.
299             # Returns the internal $state object accumulated during the traversal.
300 17944     17944 1 64821 sub traverse ($self, $schema_reference, $config_override = {}) {
  17944         24009  
  17944         22825  
  17944         24967  
  17944         20390  
301 17944         47129 my %overrides = %$config_override;
302 17944         54727 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
303 17944 50       37413 croak join(', ', sort keys %overrides), ' not supported as a config override in traverse'
304             if keys %overrides;
305              
306             # Note: the starting position is not guaranteed to be at the root of the $document,
307             # nor is the fragment portion of this uri necessarily empty
308 17944   66     81985 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
309 17944   100     5022148 my $initial_path = $config_override->{traversed_keyword_path} // '';
310 17944   100     108930 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
311              
312 17944 50       68264 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
313              
314 17944 100       41246 if (length(my $uri_path = $initial_uri->fragment)) {
315 5 50       46 croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
316              
317 5 50       28 croak 'traversed_keyword_path does not match initial_schema_uri path fragment'
318             if substr($initial_path, -length($uri_path)) ne $uri_path;
319             }
320              
321             my $state = {
322             depth => 0,
323             data_path => '', # this never changes since we don't have an instance yet
324             initial_schema_uri => $initial_uri, # the canonical URI as of the start of this method or last $id
325             traversed_keyword_path => $initial_path, # the accumulated traversal path as of the start or last $id
326             keyword_path => '', # the rest of the path, since the start of this method or last $id
327             specification_version => $spec_version,
328             errors => [],
329             identifiers => {},
330             subschemas => [],
331             $config_override->{skip_ref_checks} ? () : (references => []),
332             callbacks => $config_override->{callbacks} // {},
333 17944 100 100     275798 evaluator => $self,
334             traverse => 1,
335             };
336              
337 17944         33602 my $valid = 1;
338              
339 17944         32314 try {
340             # determine the initial value of specification_version and vocabularies, so we have something to start
341             # with in _traverse_subschema().
342             # a subsequent "$schema" keyword can still change these values, and it is always processed
343             # first, so the override is skipped if the keyword exists in the schema
344             $state->{metaschema_uri} =
345             (ref $schema_reference eq 'HASH' && exists $schema_reference->{'$schema'} ? undef
346 17944 100 100     180707 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
347              
348 17944 100       62541 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
349 17938         394724 $state->@{qw(specification_version vocabularies)} = @$metaschema_info;
350             }
351             else {
352             # metaschema has not been processed for vocabularies yet...
353              
354             die 'something went wrong - cannot get metaschema data for '.$state->{metaschema_uri}
355 7 50       1060 if not $config_override->{metaschema_uri};
356              
357             # use the Core vocabulary to set metaschema info via the '$schema' keyword implementation
358             $valid = $self->_get_metaschema_vocabulary_classes($self->METASCHEMA_URIS->{$spec_version})->[1][0]
359 7         38 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
360             }
361              
362 17944 100 66     123766 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
363 17944 50 66     39659 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
364 17944 50 66     106035 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
365             }
366             catch ($e) {
367 1 0       4 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    0          
368 1         2 push $state->{errors}->@*, $e->errors;
369             }
370             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
371             # note: we should never be here, since traversal subs are no longer fatal
372 1         9 push $state->{errors}->@*, $e;
373             }
374             else {
375 1         3 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
376             }
377             }
378              
379 17944         69905 return $state;
380             }
381              
382             # the actual runtime evaluation of the schema against input data.
383 17391     17391 1 21527414 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17391         30199  
  17391         29051  
  17391         26533  
  17391         34122  
  17391         26499  
384 17391 50       46478 croak 'evaluate called in void context' if not defined wantarray;
385              
386 17391         42848 my %overrides = %$config_override;
387 17391         54449 delete @overrides{qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict with_defaults callbacks data_path traversed_keyword_path _strict_schema_data)};
388 17391 50       37973 croak join(', ', sort keys %overrides), ' not supported as a config override in evaluate'
389             if keys %overrides;
390              
391             my $state = {
392             data_path => $config_override->{data_path} // '',
393 17391   100     164549 traversed_keyword_path => $config_override->{traversed_keyword_path} // '', # the accumulated path as of the start of evaluation or last $id or $ref
      100        
394             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start of evaluation or last $id or $ref
395             keyword_path => '', # the rest of the path, since the start of evaluation or last $id or $ref
396             errors => [],
397             depth => 0,
398             };
399              
400 17391 100       383871 $state->{data} = jsonp_set('', $state->{data_path}, ref $data ? clone($data) : $data);
401              
402 17391         27292 my $valid;
403 17390         29185 try {
404 17390 100 100     53149 if (is_schema($schema_reference)) {
    100          
405             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
406 17315         53836 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
407             }
408             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
409 4         10 abort($state, 'invalid schema type: %s', get_type($schema_reference));
410             }
411              
412 17201         161353 my $schema_info = $self->_fetch_from_uri($schema_reference);
413 17201 100       37787 abort($state, 'EXCEPTION: unable to find resource "%s"', $schema_reference)
414             if not $schema_info;
415              
416             abort($state, 'EXCEPTION: "%s" is not a schema', $schema_reference)
417 17195 100       69597 if not $schema_info->{document}->get_entity_at_location($schema_info->{document_path});
418              
419             $state = +{
420             %$state,
421             initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
422             $schema_info->%{qw(document specification_version vocabularies)},
423             dynamic_scope => [ $schema_info->{canonical_uri}->clone->fragment(undef) ],
424             annotations => [],
425             seen => {},
426             callbacks => $config_override->{callbacks} // {},
427             evaluator => $self,
428             (map {
429 120358   100     1853445 my $val = $config_override->{$_} // $self->$_;
430 120358 100       744498 defined $val ? ($_ => $val) : ()
431             # note: this is a subset of the allowed overrides defined above
432             } qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
433 17194 100 100     111735 $config_override->{with_defaults} // $self->with_defaults ? (defaults => {}) : (),
      100        
434             };
435              
436             # this hash will be added to at each level of schema evaluation
437 17194 100       66539 $state->{seen_data_properties} = {} if $config_override->{_strict_schema_data};
438              
439             # we're going to set collect_annotations during evaluation when we see an unevaluated* keyword
440             # (or for object data when the _strict_schema_data configuration is set),
441             # but after we pass to a new data scope we'll clear it again.. unless we've got the config set
442             # globally for the entire evaluation, so we store that value in a high bit.
443 17194   100     57589 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
444              
445 17194         64787 $valid = $self->_evaluate_subschema($data, $schema_info->{schema}, $state);
446 17171 50 66     55902 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
447 17171 50 66     102668 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
448             }
449             catch ($e) {
450 219 100       693 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
451 180         1336 return $e;
452             }
453             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
454 34         130 push $state->{errors}->@*, $e;
455             }
456             else {
457 5         58 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
458             }
459             }
460              
461 17210 100       44096 if ($state->{seen_data_properties}) {
462 5         13 my %unknown_keywords;
463 5         53 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
464             keys $state->{seen_data_properties}->%*) {
465 15         63 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
466 15   100     58 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
467             }
468              
469 5         23 foreach my $parent (sort keys %unknown_keywords) {
470             $valid = E({ %$state, data_path => $parent },
471             'unknown keyword%s seen in schema: %s', $unknown_keywords{$parent}->@* > 1 ? 's' : '',
472 8 100       126 join(', ', sort $unknown_keywords{$parent}->@*));
473             }
474             }
475              
476 17210 50 50     70311 die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
477              
478             return JSON::Schema::Modern::Result->new(
479             output_format => $self->output_format,
480             valid => $valid,
481             $valid
482             # strip annotations from result if user didn't explicitly ask for them
483             ? ($config_override->{collect_annotations} // $self->collect_annotations
484             ? (annotations => $state->{annotations}) : ())
485             : (errors => $state->{errors}),
486             $state->{defaults} ? (defaults => $state->{defaults}) : (),
487             data => $state->{data},
488 17210 100 100     498766 );
    100          
    100          
489             }
490              
491 10     11 1 19470 sub validate_schema ($self, $schema, $config_override = {}) {
  10         22  
  10         18  
  10         20  
  10         14  
492 10 50       34 croak 'validate_schema called in void context' if not defined wantarray;
493              
494             my $metaschema_uri = ref $schema eq 'HASH' && $schema->{'$schema'} ? $schema->{'$schema'}
495 10 100 66     137 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
496              
497             my $result = $self->evaluate($schema, $metaschema_uri,
498 10 100 100     95 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
499              
500 10 100       275 return $result if not $result->valid;
501              
502             # the traversal pass will validate all constraints that weren't handled by the metaschema
503 3         80 my $state = $self->traverse($schema);
504             return JSON::Schema::Modern::Result->new(
505             output_format => $self->output_format,
506             valid => 0,
507             errors => $state->{errors},
508 3 100       33 ) if $state->{errors}->@*;
509              
510 2         34 return $result; # valid: true
511             }
512              
513 8     9 1 30397 sub get ($self, $uri_reference) {
  8         15  
  8         15  
  8         13  
514 8 100       49 if (wantarray) {
515 5         19 my $schema_info = $self->_fetch_from_uri($uri_reference);
516 5 100       61 return if not $schema_info;
517 4 100       308 my $subschema = ref $schema_info->{schema} ? clone($schema_info->{schema}) : $schema_info->{schema};
518 4         34 return ($subschema, $schema_info->{canonical_uri});
519             }
520             else { # abridged version of _fetch_from_uri
521 3 50       22 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
522 3         407 my $fragment = $uri_reference->fragment;
523 3         16 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
524 3 50       153 return if not $resource;
525              
526 3         6 my $schema;
527 3 100 100     18 if (not length($fragment) or $fragment =~ m{^/}) {
528 2   100     23 $schema = $resource->{document}->get($resource->{path}.($fragment//''));
529             }
530             else { # we are following a URI with a plain-name fragment
531 1 50 50     33 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
532 0         0 $schema = $resource->{document}->get($subresource->{path});
533             }
534 2 100       150 return ref $schema ? clone($schema) : $schema;
535             }
536             }
537              
538 0     1 1 0 sub get_document ($self, $uri_reference) {
  0         0  
  0         0  
  0         0  
539 0         0 my $schema_info = $self->_fetch_from_uri($uri_reference);
540 0 0       0 return if not $schema_info;
541 0         0 return $schema_info->{document};
542             }
543              
544             # defined lower down:
545             # sub add_media_type ($self, $media_type, $sub) { ... }
546             # sub get_media_type ($self, $media_type) { ... }
547             # sub add_encoding ($self, $encoding, $sub) { ... }
548             # sub get_encoding ($self, $encoding) { ... }
549             # sub add_vocabulary ($self, $classname) { ... }
550              
551             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
552              
553             # current spec version => { keyword => undef, or arrayref of alternatives }
554             my %removed_keywords = (
555             'draft4' => {
556             },
557             'draft6' => {
558             id => [ '$id' ],
559             },
560             'draft7' => {
561             id => [ '$id' ],
562             },
563             'draft2019-09' => {
564             id => [ '$id' ],
565             definitions => [ '$defs' ],
566             dependencies => [ qw(dependentSchemas dependentRequired) ],
567             },
568             'draft2020-12' => {
569             id => [ '$id' ],
570             definitions => [ '$defs' ],
571             dependencies => [ qw(dependentSchemas dependentRequired) ],
572             '$recursiveAnchor' => [ '$dynamicAnchor' ],
573             '$recursiveRef' => [ '$dynamicRef' ],
574             additionalItems => [ 'items' ],
575             },
576             );
577              
578             # {
579             # $spec_version => {
580             # $vocabulary_class => {
581             # traverse => [ [ $keyword => $subref ], [ ... ] ],
582             # evaluate => [ [ $keyword => $subref ], [ ... ] ],
583             # }
584             # }
585             # }
586             # If we could serialize coderefs, this could be an object attribute;
587             # otherwise, we might as well persist this for the lifetime of the process.
588             our $vocabulary_cache = {};
589              
590 42172     42173   59741 sub _traverse_subschema ($self, $schema, $state) {
  42172         49297  
  42172         48250  
  42172         47760  
  42172         45146  
591 42172         294451 delete $state->@{'keyword', grep /^_/, keys %$state};
592              
593             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_depth)
594 42172 50       162932 if $state->{depth}++ > $self->max_depth;
595              
596 42172         144724 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
597              
598 42172         106749 my $schema_type = get_type($schema);
599             return 1 if $schema_type eq 'boolean'
600             and ($state->{specification_version} ne 'draft4'
601 42172 100 100     129002 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
602              
603 34462 100       63189 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
604              
605 34445 100       81945 return 1 if not keys %$schema;
606              
607 33246         46436 my $valid = 1;
608 33246         166624 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
609              
610             # we use an index rather than iterating through the lists directly because the lists of
611             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
612             # and $schema keyword can make this change, and they both come first, therefore a simple index
613             # into the list is sufficient.
614             ALL_KEYWORDS:
615 33246         95429 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
616 203351         320621 my $vocabulary = $state->{vocabularies}[$vocab_index];
617 203351         197930 my $keyword_list;
618              
619 203351   66     305663 for (my $keyword_index = 0;
620             $keyword_index < ($keyword_list //= do {
621 48     48   146620 use autovivification qw(fetch store);
  48         105  
  48         233  
622             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{traverse} //= [
623             map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
624             $vocabulary->keywords($state->{specification_version})
625 203419   100     617864 ];
626             })->@*;
627             $keyword_index++) {
628 1684836         2260909 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
629 1684836 100       3518217 next if not exists $schema->{$keyword};
630              
631             # keywords adjacent to $ref are not evaluated before draft2019-09
632 56872 100 100     198243 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
633              
634 56836         89373 delete $unknown_keywords{$keyword};
635 56836         97193 $state->{keyword} = $keyword;
636              
637 56836         80548 my $old_spec_version = $state->{specification_version};
638 56836         84189 my $error_count = $state->{errors}->@*;
639              
640 56836 100       182290 if (not $sub->($vocabulary, $schema, $state)) {
641             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
642 243 50       762 if $error_count == $state->{errors}->@*;
643 243         395 $valid = 0;
644 243         887 next;
645             }
646             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
647 56593 50       172404 if $error_count != $state->{errors}->@*;
648              
649             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
650 56593 100       107704 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
651              
652 56593 100       212988 if (my $callback = $state->{callbacks}{$keyword}) {
653 4         7 $error_count = $state->{errors}->@*;
654              
655 4 50       12 if (not $callback->($schema, $state)) {
656             die 'callback result is false but there are no errors (keyword: '.$keyword.')'
657 0 0       0 if $error_count == $state->{errors}->@*;
658 0         0 $valid = 0;
659 0         0 next;
660             }
661             die 'callback result is true but there are errors (keyword: '.$keyword.')'
662 4 50       2018 if $error_count != $state->{errors}->@*;
663             }
664             }
665             }
666              
667 33246         64747 delete $state->{keyword};
668              
669 33246 100 100     109770 if ($self->strict and keys %unknown_keywords) {
670 2 50       12 $valid = E($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
671             join(', ', sort keys %unknown_keywords));
672             }
673              
674             # check for previously-supported but now removed keywords
675 33246         157786 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
676 104050 100       177537 next if not exists $schema->{$keyword};
677 224         831 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
678             .canonical_uri($state).'")';
679 224 50       23995 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
680 224         960 my @list = map '"'.$_.'"', @$alternates;
681 224 50       575 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
682 224 100       655 splice(@list, -1, 0, 'or') if @list > 1;
683 224         606 $message .= ': this should be rewritten as '.join(' ', @list);
684             }
685 224         50838 carp $message;
686             }
687              
688 33246         147151 return $valid;
689             }
690              
691 34834     34835   44581 sub _evaluate_subschema ($self, $data, $schema, $state) {
  34834         44868  
  34834         45773  
  34834         42482  
  34834         44490  
  34834         38566  
692 34834 50       67417 croak '_evaluate_subschema called in void context' if not defined wantarray;
693              
694             # callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
695             # paths; but annotations, errors are arrayrefs so their contents will be shared
696 34834   100     123874 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
697 34834         279582 delete $state->@{'keyword', grep /^_/, keys %$state};
698              
699             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_depth)
700 34834 100       137554 if $state->{depth}++ > $self->max_depth;
701              
702 34831         104729 my $schema_type = get_type($schema);
703 34831 100 66     71306 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
704              
705             # this should never happen, due to checks in traverse
706 33987 50       58832 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
707              
708 33987 100       77153 return 1 if not keys %$schema;
709              
710             # find all schema locations in effect at this data path + uri combination
711             # if any of them are absolute prefix of this schema location, we are in a loop.
712 33349         87756 my $canonical_uri = canonical_uri($state);
713 33349         80311 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
714             {
715 48     48   50858 use autovivification qw(fetch store);
  48         100  
  48         213  
  33349         40291  
716             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
717             if grep substr($schema_location, 0, length) eq $_,
718 33349 100       125066 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
719 33348         4037223 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
720             }
721              
722 33348         3191489 my $valid = 1;
723 33348         213858 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
724              
725             # set aside annotations collected so far; they are not used in the current scope's evaluation
726 33348         74104 my $parent_annotations = $state->{annotations};
727 33348         65519 $state->{annotations} = [];
728              
729             # in order to collect annotations from applicator keywords only when needed, we twiddle the low
730             # bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
731             # We also set it when _strict_schema_data is set, but only for object data instances.
732             $state->{collect_annotations} |=
733             0+((ref $data eq 'ARRAY' && exists $schema->{unevaluatedItems})
734             || ((my $is_object_data = ref $data eq 'HASH')
735 33348   100     245847 && (exists $schema->{unevaluatedProperties} || !!$state->{seen_data_properties})));
736              
737             # set aside defaults collected so far; we need to keep the subschema's defaults separated in
738             # case they must be discarded due to overall invalidity of the subschema
739 33348         60274 my $defaults = $state->{defaults};
740 33348 100       68976 $state->{defaults} = {} if $state->{defaults};
741              
742             # we use an index rather than iterating through the lists directly because the lists of
743             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
744             # and $schema keyword can make this change, and they both come first, therefore a simple index
745             # into the list is sufficient.
746              
747             ALL_KEYWORDS:
748 33348         93629 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
749 183051         284654 my $vocabulary = $state->{vocabularies}[$vocab_index];
750 183051         182424 my $keyword_list;
751              
752 183051   66     271347 for (my $keyword_index = 0;
753             $keyword_index < ($keyword_list //= do {
754 48     48   15745 use autovivification qw(fetch store);
  48         108  
  48         209  
755             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{evaluate} //= [
756             map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
757             $vocabulary->keywords($state->{specification_version})
758 183054   100     589636 ];
759             })->@*;
760             $keyword_index++) {
761 1510156         1991257 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
762 1510156 100       3108339 next if not exists $schema->{$keyword};
763              
764             # keywords adjacent to $ref are not evaluated before draft2019-09
765 60037 100 100     201452 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
766              
767 60006         97364 delete $unknown_keywords{$keyword};
768 60006 100 100     113961 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
769              
770 60005         110346 $state->{keyword} = $keyword;
771              
772 60005 100       93062 if ($sub) {
773 55929         90217 my $old_spec_version = $state->{specification_version};
774 55929         89877 my $error_count = $state->{errors}->@*;
775              
776 55929         78745 try {
777 55929 100       176797 if (not $sub->($vocabulary, $data, $schema, $state)) {
778             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
779 13161 50       39585 if $error_count == $state->{errors}->@*;
780 13161         18969 $valid = 0;
781              
782 13161 100 100     59886 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
783 6948         29937 next;
784             }
785              
786             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
787 42724 50       276486 if $error_count != $state->{errors}->@*;
788             }
789             catch ($e) {
790 44 100       248 die $e if $e->$_isa('JSON::Schema::Modern::Error');
791 2         11 abort($state, 'EXCEPTION: '.$e);
792             }
793              
794             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
795 42724 100       96441 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
796             }
797              
798 46800 100 100     178229 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
799 24         41 my $error_count = $state->{errors}->@*;
800              
801 24 100       68 if (not $callback->($data, $schema, $state)) {
802             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
803 2 50       8 if $error_count == $state->{errors}->@*;
804 2         3 $valid = 0;
805              
806 2 100 66     14 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
807 1         4 next;
808             }
809             warn 'callback result is true but there are errors (keyword: '.$keyword.')'
810 22 50       538 if $error_count != $state->{errors}->@*;
811             }
812             }
813             }
814              
815 33304         66953 delete $state->{keyword};
816              
817 33304 100 100     68013 if ($state->{strict} and keys %unknown_keywords) {
818 3 100       27 abort($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
819             join(', ', sort keys %unknown_keywords));
820             }
821              
822             # Note: we can remove all of this entirely and just rely on strict mode when we (eventually!) remove
823             # the traverse phase and replace with evaluate-against-metaschema.
824 33301 100 100     72932 if ($state->{seen_data_properties} and $is_object_data) {
825             # record the locations of all local properties
826             $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 0
827 156         1011 foreach grep !/^x-/, keys %$data;
828              
829             my @evaluated_properties = map {
830 156         381 my $keyword = $_->{keyword};
  577         713  
831             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
832 577 100       1307 ? $_->{annotation}->@* : ();
833             } local_annotations($state);
834              
835             # tick off properties that were recognized by this subschema
836 156         327 $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 1 foreach @evaluated_properties;
837              
838             # weird! the draft4 metaschema doesn't know about '$ref' at all!
839             $state->{seen_data_properties}{$state->{data_path}.'/$ref'} |= 1
840 156 100 66     440 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
841             }
842              
843 33301 100 100     96901 if ($valid and $state->{collect_annotations} and $state->{specification_version} !~ /^draft(?:[467]|2019-09)\z/) {
      100        
844             annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
845 969         2841 foreach sort keys %unknown_keywords;
846             }
847              
848             # only keep new annotations if schema is valid
849 33301 100       71746 push $parent_annotations->@*, $state->{annotations}->@* if $valid;
850              
851             # only keep new defaults if schema is valid
852             $defaults->@{keys $state->{defaults}->%*} = values $state->{defaults}->%*
853 33301 100 100     78488 if $valid and $state->{defaults};
854              
855 33301         228482 return $valid;
856             }
857              
858             has _resource_index => (
859             is => 'bare',
860             isa => Map[my $resource_key_type = Str->where('!/#/'), my $resource_type = Dict[
861             canonical_uri => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
862             path => json_pointer_type, # JSON pointer relative to the document root
863             specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
864             document => InstanceOf['JSON::Schema::Modern::Document'],
865             # the vocabularies used when evaluating instance data against schema
866             vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
867             anchors => Optional[HashRef[Dict[
868             canonical_uri => canonical_uri_type, # equivalent uri with json pointer fragment
869             path => json_pointer_type, # JSON pointer relative to the document root
870             dynamic => Optional[Bool],
871             ]]],
872             Slurpy[HashRef[Undef]], # no other fields allowed
873             ]],
874             );
875              
876             sub _get_resource {
877 44037 50   44038   107091 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
878 44037   100     2878728 ($_[0]->{_resource_index}//{})->{$_[1]}
879             }
880              
881             # does not check for duplicate entries, or for malformed uris
882             sub _add_resources_unsafe {
883 48     48   68564 use autovivification 'store';
  48         216  
  48         231  
884             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
885 103     104   1082 foreach pairs @_[1..$#_];
886             }
887 25   50 26   11479 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
888 17395   100 17396   2949853 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
889 2123   50 2124   248261 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
890              
891 18978     18979   43346 sub _add_resource ($self, @kvs) {
  18978         26900  
  18978         34199  
  18978         25438  
892 18978         78255 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
893 18978         35538 my ($canonical_uri, $resource) = @$pair;
894              
895 18978 100       51232 if (my $existing = $self->_get_resource($canonical_uri)) {
    100          
896             # we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
897             # lack all identifiers altogether, but preserve other resources from the original document
898 17056 100       51754 if ($canonical_uri ne '') {
899             my @diffs = (
900             ($existing->{path} eq $resource->{path} ? () : 'path'),
901             ($existing->{canonical_uri} eq $resource->{canonical_uri} ? () : 'canonical_uri'),
902             ($existing->{specification_version} eq $resource->{specification_version} ? () : 'specification_version'),
903 859 100       5442 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
904 859 100       232311 next if not @diffs;
905 10         2784 croak 'uri "'.$canonical_uri.'" conflicts with an existing schema resource: documents differ by ',
906             join(', ', @diffs);
907             }
908             }
909             elsif (JSON::Schema::Modern::Utilities::get_schema_filename($canonical_uri)) {
910 2         628 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
911             }
912              
913 48     48   24013 use autovivification 'store';
  48         87  
  48         199  
914 18117         89269 $self->{_resource_index}{$resource_key_type->($canonical_uri)} = $resource_type->($resource);
915             }
916             }
917              
918             # $vocabulary uri (not its $id!) => [ specification_version, class ]
919             has _vocabulary_classes => (
920             is => 'bare',
921             isa => HashRef[
922             my $vocabulary_type = Tuple[
923             $spec_version_type,
924             $vocabulary_class_type,
925             ]
926             ],
927             reader => '__vocabulary_classes',
928             lazy => 1,
929             default => sub {
930             +{
931 12     13   825 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   36  
  12     13   240  
  12     12   524  
  12     12   30  
  12     12   170  
  12     12   608  
  12     12   18  
  12     7   148  
  12     7   529  
  12     7   27  
  12     7   184  
  12     7   496  
  12     7   37  
  12     7   179  
  12     7   461  
  12     1   17  
  12     1   138  
  12     1   66  
  12     1   16  
  12     1   138  
  12     1   35  
  12         17  
  12         34  
  7         47  
  7         17  
  7         118  
  7         30  
  7         11  
  7         121  
  7         30  
  7         14  
  7         87  
  7         25  
  7         14  
  7         77  
  7         19  
  7         25  
  7         116  
  7         23  
  7         12  
  7         62  
  7         35  
  7         9  
  7         78  
  7         23  
  7         9  
  7         31  
  1         4  
  1         2  
  1         9  
  1         3  
  1         1  
  1         9  
  1         5  
  1         1  
  1         8  
  1         3  
  1         2  
  1         8  
  1         2  
  1         2  
  1         25  
  1         5  
  1         1  
  1         4  
932             map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
933             qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
934             }
935             },
936             );
937              
938 107     108   2035 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
939              
940 10     11 1 23863 sub add_vocabulary ($self, $classname) {
  10         23  
  10         18  
  10         15  
941 10 50       301 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
942              
943 10         1026 $vocabulary_class_type->(load_module($classname));
944              
945             # uri => version, uri => version
946 7         542 foreach my $pair (pairs $classname->vocabulary) {
947 7         90 my ($uri_string, $spec_version) = @$pair;
948 7         44 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
949 6         7580 $spec_version_type->($spec_version);
950              
951 4 100       499 croak 'keywords starting with "$" are reserved for core and cannot be used'
952             if grep /^\$/, $classname->keywords;
953              
954 3         26 $self->{_vocabulary_classes}{$uri_string} = $vocabulary_type->([ $spec_version, $classname ]);
955             }
956             }
957              
958             # $schema uri => [ specification_version, [ vocab classes, in evaluation order ] ].
959             has _metaschema_vocabulary_classes => (
960             is => 'bare',
961             isa => HashRef[
962             my $mvc_type = Tuple[
963             $spec_version_type,
964             ArrayRef[$vocabulary_class_type],
965             ]
966             ],
967             reader => '__metaschema_vocabulary_classes',
968             lazy => 1,
969             default => sub {
970 38     38   21647 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   119  
  38     38   1055  
  38     38   16369  
  38     38   181  
  38     38   1182  
  38     38   19577  
  38     30   156  
  38     30   1084  
  38     30   17922  
  38     30   154  
  38     30   979  
  38     30   16311  
  38     30   132  
  38     19   1238  
  38     19   15795  
  38     19   126  
  38     19   998  
  38     19   709  
  38     19   76  
  38     19   175  
  30     17   235  
  30     16   115  
  30     16   442  
  30     16   139  
  30     16   50  
  30     16   321  
  30     16   109  
  30     13   61  
  30     13   379  
  30     13   146  
  30     13   47  
  30     13   357  
  30     13   95  
  30     13   59  
  30     12   410  
  30     12   133  
  30     12   44  
  30     12   375  
  30     12   359  
  30     12   51  
  30     12   111  
  19     11   190  
  19     11   35  
  19     11   387  
  19     11   113  
  19     11   38  
  19     11   243  
  19     11   89  
  19     10   34  
  19     10   210  
  19     10   129  
  19     10   42  
  19     9   213  
  19     9   61  
  19     9   37  
  19     9   180  
  19     9   93  
  19     9   32  
  19     9   200  
  19     9   73  
  19     9   36  
  19     9   67  
  17     9   155  
  17     9   31  
  17     9   301  
  16     9   78  
  16     9   31  
  16     9   169  
  16     9   58  
  16     8   30  
  16     8   251  
  16     8   82  
  16     8   32  
  16     8   143  
  16     8   51  
  16     8   35  
  16     6   223  
  16     6   76  
  16     6   27  
  16     6   152  
  16     6   59  
  16     6   32  
  16     6   55  
  13     6   100  
  13     6   31  
  13     6   164  
  13     6   57  
  13     6   22  
  13     6   139  
  13     6   69  
  13     6   27  
  13     6   269  
  13     6   77  
  13     5   23  
  13     5   236  
  13     5   47  
  13     5   28  
  13     5   196  
  13     5   62  
  13     5   23  
  13     5   164  
  13     5   63  
  13     5   26  
  13     5   49  
  12     6   77  
  12     6   26  
  12     6   144  
  12     6   53  
  12     6   21  
  12     6   111  
  12     6   72  
  12     6   32  
  12     4   173  
  12     4   51  
  12     4   21  
  12     4   97  
  12     4   46  
  12     4   48  
  12     4   153  
  12     4   53  
  12     4   22  
  12     4   158  
  12     4   49  
  12     4   22  
  12     4   56  
  11         89  
  11         24  
  11         203  
  11         73  
  11         23  
  11         114  
  11         55  
  11         20  
  11         185  
  11         59  
  11         22  
  11         171  
  11         40  
  11         25  
  11         163  
  11         68  
  11         22  
  11         165  
  11         57  
  11         23  
  11         44  
  10         79  
  10         24  
  10         124  
  10         86  
  10         23  
  10         157  
  10         70  
  10         21  
  10         114  
  10         58  
  10         20  
  10         110  
  10         36  
  9         21  
  9         131  
  9         44  
  9         17  
  9         128  
  9         42  
  9         15  
  9         33  
  9         57  
  9         18  
  9         166  
  9         40  
  9         21  
  9         103  
  9         53  
  9         17  
  9         170  
  9         39  
  9         17  
  9         107  
  9         32  
  9         17  
  9         103  
  9         3205  
  9         18  
  9         106  
  9         35  
  9         14  
  9         31  
  9         70  
  9         33  
  9         159  
  9         54  
  9         19  
  9         145  
  9         45  
  9         17  
  9         147  
  9         45  
  9         19  
  9         170  
  9         35  
  9         53  
  9         100  
  9         47  
  9         20  
  9         135  
  9         44  
  9         15  
  9         63  
  8         64  
  8         16  
  8         113  
  8         34  
  8         14  
  8         119  
  8         37  
  8         35  
  8         86  
  8         29  
  8         17  
  8         86  
  8         27  
  8         16  
  8         71  
  8         31  
  8         16  
  8         78  
  8         47  
  8         28  
  8         26  
  6         41  
  6         12  
  6         83  
  6         32  
  6         10  
  6         65  
  6         26  
  6         10  
  6         68  
  6         29  
  6         25  
  6         130  
  6         22  
  6         14  
  6         80  
  6         26  
  6         13  
  6         44  
  6         23  
  6         13  
  6         21  
  6         38  
  6         14  
  6         65  
  6         24  
  6         9  
  6         47  
  6         36  
  6         10  
  6         54  
  6         23  
  6         9  
  6         78  
  6         21  
  6         12  
  6         52  
  6         26  
  6         12  
  6         52  
  6         24  
  6         10  
  6         24  
  6         473  
  6         3849  
  6         94  
  6         481  
  6         3718  
  6         44  
  6         413  
  6         3325  
  6         42  
  5         19  
  5         10  
  5         44  
  5         18  
  5         12  
  5         72  
  5         27  
  5         8  
  5         60  
  5         22  
  5         10  
  5         14  
  5         41  
  5         11  
  5         68  
  5         24  
  5         8  
  5         42  
  5         19  
  5         7  
  5         48  
  5         21  
  5         11  
  5         65  
  5         16  
  5         12  
  5         37  
  5         24  
  5         9  
  5         49  
  5         24  
  5         8  
  5         15  
  6         41  
  6         9  
  6         63  
  6         23  
  6         11  
  6         64  
  6         22  
  6         8  
  6         46  
  6         22  
  6         10  
  6         41  
  6         17  
  6         11  
  6         100  
  6         28  
  6         13  
  6         62  
  6         24  
  6         9  
  6         34  
  6         29  
  6         12  
  6         44  
  4         16  
  4         11  
  4         28  
  4         17  
  4         7  
  4         308  
  4         35  
  4         10  
  4         25  
  4         11  
  4         9  
  4         24  
  4         22  
  4         9  
  4         53  
  4         17  
  4         8  
  4         11  
  4         25  
  4         10  
  4         55  
  4         16  
  4         7  
  4         29  
  4         15  
  4         10  
  4         54  
  4         33  
  4         9  
  4         34  
  4         13  
  4         8  
  4         47  
  4         21  
  4         8  
  4         36  
  4         35  
  4         7  
  4         12  
971             qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated);
972             +{
973             'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
974             do { pop @modules; () }, # remove Unevaluated
975             'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', [ @modules ] ],
976             'http://json-schema.org/draft-07/schema' => [ 'draft7', [ @modules ] ],
977             do { splice @modules, 4, 1; () }, # remove Content
978             'http://json-schema.org/draft-06/schema' => [ 'draft6', \@modules ],
979             'http://json-schema.org/draft-04/schema' => [ 'draft4', \@modules ],
980             },
981             },
982             );
983              
984 30503     30504   644071 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
985 5924     5925   31304 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
986 4     5   187 sub __all_metaschema_vocabulary_classes { values $_[0]->__metaschema_vocabulary_classes->%* }
987              
988             # translate vocabulary URIs into classes, caching the results (if any)
989 53     54   140 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         88  
  53         78  
  53         69  
  53         83  
990 53 100       238 if (not exists $schema_info->{schema}{'$vocabulary'}) {
991             # "If "$vocabulary" is absent, an implementation MAY determine behavior based on the meta-schema
992             # if it is recognized from the URI value of the referring schema's "$schema" keyword."
993 2         14 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
994 2         9 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
995             }
996              
997 51         97 my $valid = 1;
998             # Core §8.1.2-6: "The "$vocabulary" keyword SHOULD be used in the root schema of any schema
999             # document intended for use as a meta-schema. It MUST NOT appear in subschemas."
1000 51 100       182 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
1001 51 100       205 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
1002              
1003 51 100       133 return (undef, []) if not $valid;
1004              
1005 49         92 my @vocabulary_classes;
1006              
1007 49         293 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
1008 105         254 my $class_info = $self->_get_vocabulary_class($uri);
1009             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" is not a known vocabulary', $uri), next
1010 105 100 100     9692 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1011              
1012 97 100       816 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1013              
1014 89         178 my ($spec_version, $class) = @$class_info;
1015             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" uses %s, but the metaschema itself uses %s',
1016             $uri, $spec_version, $schema_info->{specification_version}), next
1017 89 100       314 if $spec_version ne $schema_info->{specification_version};
1018              
1019 83         168 push @vocabulary_classes, $class;
1020             }
1021              
1022             @vocabulary_classes = sort {
1023 49 50       194 $a->evaluation_order <=> $b->evaluation_order
  49 50       171  
1024             || ($a->evaluation_order == 999 ? 0
1025             : ($valid = E($state, '%s and %s have a conflicting evaluation_order', sort $a, $b)))
1026             } @vocabulary_classes;
1027              
1028 49 100 100     271 $valid = E($state, 'the first vocabulary (by evaluation_order) must be Core')
1029             if ($vocabulary_classes[0]//'') ne 'JSON::Schema::Modern::Vocabulary::Core';
1030              
1031 49         78 my %seen_keyword;
1032 49         99 foreach my $class (@vocabulary_classes) {
1033 83         286 foreach my $keyword ($class->keywords($schema_info->{specification_version})) {
1034             $valid = E($state, '%s keyword "%s" conflicts with keyword of the same name from %s',
1035             $class, $keyword, $seen_keyword{$keyword})
1036 794 100       1149 if $seen_keyword{$keyword};
1037 794         1441 $seen_keyword{$keyword} = $class;
1038             }
1039             }
1040              
1041 49 100       537 return ($schema_info->{specification_version}, $valid ? \@vocabulary_classes : []);
1042             }
1043              
1044             # used for determining a default '$schema' keyword where there is none
1045             # these are also normalized as this is how we cache them
1046 48         5811 use constant METASCHEMA_URIS => {
1047             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
1048             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
1049             'draft7' => 'http://json-schema.org/draft-07/schema',
1050             'draft6' => 'http://json-schema.org/draft-06/schema',
1051             'draft4' => 'http://json-schema.org/draft-04/schema',
1052 48     48   68360 };
  48         93  
1053              
1054             # for internal use only. files are under share/
1055 48         48030 use constant _CACHED_METASCHEMAS => {
1056             'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
1057             'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
1058             'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
1059             'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
1060             'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
1061             'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
1062             'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
1063             'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
1064             'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
1065             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
1066              
1067             'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
1068             'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
1069             'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
1070             'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
1071             'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
1072             'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
1073             'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
1074             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
1075              
1076             # trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
1077             'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
1078             'http://json-schema.org/draft-06/schema' => 'draft6/schema.json',
1079             'http://json-schema.org/draft-04/schema' => 'draft4/schema.json',
1080 48     48   271 };
  48         89  
1081              
1082             # simple runtime-wide cache of metaschema document objects that are sourced from disk
1083             my $metaschema_cache = {};
1084              
1085             {
1086             my $share_dir = dist_dir('JSON-Schema-Modern');
1087             JSON::Schema::Modern::Utilities::register_schema($_, $share_dir.'/'._CACHED_METASCHEMAS->{$_})
1088             foreach keys _CACHED_METASCHEMAS->%*;
1089             }
1090              
1091             # returns the same as _get_resource
1092 24428     24429   1615398 sub _get_or_load_resource ($self, $uri) {
  24428         32871  
  24428         28193  
  24428         27885  
1093 24428         55405 my $resource = $self->_get_resource($uri);
1094 24428 100       2197593 return $resource if $resource;
1095              
1096 114 100       511 if (my $document = load_cached_document($self, $uri)) {
1097 103         365 return $self->_get_resource($uri);
1098             }
1099              
1100             # TODO:
1101             # - load from network or disk
1102              
1103 11         25 return;
1104             };
1105              
1106             # returns information necessary to use a schema found at a particular URI or uri-reference:
1107             # - schema: a schema (which may not be at a document root)
1108             # - canonical_uri: the canonical uri for that schema,
1109             # - document: the JSON::Schema::Modern::Document object that holds that schema
1110             # - document_path: the path relative to the document root for this schema
1111             # - specification_version: the specification version that applies to this schema
1112             # - vocabularies: the vocabularies to use when considering schema keywords
1113             # creates a Document and adds it to the resource index, if not already present.
1114 24190     24191   227556 sub _fetch_from_uri ($self, $uri_reference) {
  24190         34448  
  24190         31592  
  24190         29353  
1115 24190 100       60660 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
1116              
1117             # this is *a* resource that would contain our desired location, but may not be the closest one
1118 24190         89881 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1119 24190 100       125723 return if not $resource;
1120              
1121 24179         47192 my $fragment = $uri_reference->fragment;
1122 24179 100 100     119851 if (not length($fragment) or $fragment =~ m{^/}) {
1123 23599   100     192029 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1124 23599 100       178204 return if not defined $subschema;
1125              
1126 23597         31427 my $closest_resource;
1127 23597 100       44401 if (not length $fragment) { # we already have the canonical resource root
1128 21474         43951 $closest_resource = [ undef, $resource ];
1129             }
1130             else {
1131             # determine the canonical uri by finding the closest schema resource(s)
1132 2123         5620 my $doc_addr = refaddr($resource->{document});
1133             my @closest_resources =
1134 499         2600 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1135             grep { !length($_->[1]{path}) # document root
1136 2913 100 66     25131 || length($document_path)
1137             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1138 2123         6891 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228352         367761  
1139             $self->_resource_pairs;
1140              
1141             # now whittle down to all the resources with the same document path as the first candidate
1142 2123 100       36801 if (@closest_resources > 1) {
1143             # find the resource key that most closely matches the original query uri, by matching prefixes
1144 464         2069 my $match = $uri_reference.'';
1145             @closest_resources =
1146 26         113 sort { _prefix_match_length($b->[0], $match) <=> _prefix_match_length($a->[0], $match) }
1147             grep $_->[1]{path} eq $closest_resources[0]->[1]{path},
1148 464         89052 @closest_resources;
1149             }
1150              
1151 2123         4309 $closest_resource = $closest_resources[0];
1152             }
1153              
1154             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1155 23597         86672 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1156 23597 100       1400074 $canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
1157              
1158             return {
1159             schema => $subschema,
1160             canonical_uri => $canonical_uri,
1161             document_path => $document_path,
1162 23597         301457 $closest_resource->[1]->%{qw(document specification_version vocabularies)}, # reference, not copy
1163             };
1164             }
1165             else { # we are following a URI with a plain-name fragment
1166 580 100 100     3201 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
1167             return {
1168             schema => $resource->{document}->get($subresource->{path}),
1169             canonical_uri => $subresource->{canonical_uri}, # this is *not* the anchor-containing URI
1170             document_path => $subresource->{path},
1171 578         3140 $resource->%{qw(document specification_version vocabularies)}, # reference, not copy
1172             };
1173             }
1174             }
1175              
1176             # given two strings, determines the number of characters in common, starting from the first
1177             # character
1178 52     53   64 sub _prefix_match_length ($x, $y) {
  52         68  
  52         72  
  52         55  
1179 52         120 my $len = min(length($x), length($y));
1180 52         112 foreach my $pos (0..$len) {
1181 1456 100       2130 return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
1182             }
1183 0         0 return $len;
1184             }
1185              
1186 48     48   2422 use constant _JSON_BACKEND => JSON::Schema::Modern::Utilities::_JSON_BACKEND;
  48         1019  
  48         49611  
1187              
1188             # used for internal encoding as well (when caching serialized schemas)
1189             has _json_decoder => (
1190             is => 'ro',
1191             isa => HasMethods[qw(encode decode)],
1192             lazy => 1,
1193             default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
1194             );
1195              
1196             # since media types are case-insensitive, all legacy type names must be casefolded on insertion.
1197             has _media_type => (
1198             is => 'bare',
1199             isa => ArrayRef[my $media_type_type = Str->where(q{$_ eq CORE::fc($_)})],
1200             reader => '__media_type',
1201             lazy => 1,
1202             default => sub ($self) { [] },
1203             );
1204              
1205             my ($warn_add_media_type, $warn_get_media_type); # we will warn just once
1206              
1207             # deprecated interface
1208 4     5 1 2536 sub add_media_type ($self, $media_type, $decoder) {
  4         6  
  4         5  
  4         4  
  4         5  
1209 4         65 $media_type_type->($media_type);
1210              
1211 4 100       1253 carp '$jsm->add_media_type is deprecated; use the function in JSON::Schema::Modern::Utilities instead' if not $warn_add_media_type++;
1212              
1213             # backcompat preservation: add to the global registry, and remove it again when this object goes
1214             # out of scope
1215 4         18 JSON::Schema::Modern::Utilities::add_media_type($media_type, $decoder, undef, refaddr $self);
1216 4         82 push $self->__media_type->@*, $media_type;
1217 4         38 return;
1218             }
1219              
1220 245     246   8330376 sub DESTROY ($self) {
  245         459  
  245         345  
1221 245         6365 foreach my $media_type (uniqstr $self->__media_type->@*) {
1222 4         21 JSON::Schema::Modern::Utilities::delete_media_type($media_type, refaddr $self);
1223             }
1224             }
1225              
1226             # deprecated interface; will use global definitions
1227 24     25 1 26717 sub get_media_type ($self, $type) {
  24         36  
  24         37  
  24         32  
1228 24 100       637 carp '$jsm->get_media_type is deprecated; use the function in JSON::Schema::Modern::Utilities instead' if not $warn_get_media_type++;
1229              
1230 24         595 JSON::Schema::Modern::Utilities::_get_media_type_decoder($type);
1231             }
1232              
1233             has _encoding => (
1234             is => 'bare',
1235             isa => HashRef[CodeRef],
1236             reader => '__encoding',
1237             lazy => 1,
1238             default => sub ($self) {
1239             +{
1240             identity => sub ($content_ref, @) { $content_ref },
1241             base64 => sub ($content_ref, @) {
1242             die "invalid characters\n"
1243             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
1244             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
1245             },
1246             base64url => sub ($content_ref, @) {
1247             die "invalid characters\n"
1248             if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
1249             require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
1250             },
1251             };
1252             },
1253             );
1254              
1255 23     24 1 571 sub get_encoding { $_[0]->__encoding->{$_[1]} }
1256 0     1 1 0 sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
1257              
1258             # callback hook for Sereal::Encoder
1259 3     4 0 664 sub FREEZE ($self, $serializer) {
  3         6  
  3         4  
  3         5  
1260 3         27 my $data = +{ %$self };
1261             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1262             # coderefs can't serialize cleanly and must be re-added by the user.
1263 3         15 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1264 3         85 return $data;
1265             }
1266              
1267             # callback hook for Sereal::Decoder
1268 4     5 0 212 sub THAW ($class, $serializer, $data) {
  4         8  
  4         7  
  4         7  
  4         22  
1269 4 50       16 $data->{max_depth} = delete $data->{max_traversal_depth} if exists $data->{max_traversal_depth};
1270 4         9 my $self = bless($data, $class);
1271              
1272             # load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
1273             load_module($_)
1274 4         17 foreach uniqstr(
1275             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1276             (map $_->[1], values $self->__vocabulary_classes->%*));
1277              
1278 4         51 return $self;
1279             }
1280              
1281             1;
1282              
1283             __END__