File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 1020 1034 98.6
branch 265 318 83.3
condition 160 184 86.9
subroutine 229 229 100.0
pod 14 16 87.5
total 1688 1781 94.7


line stmt bran cond sub pod time code
1 50     50   14550573 use strict;
  50         98  
  50         1753  
2 50     50   223 use warnings;
  50         84  
  50         4025  
3             package JSON::Schema::Modern; # git description: v0.637-12-gff7414b1
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.638';
9              
10 50     50   804 use 5.020; # for fc, unicode_strings features
  50         208  
11 50     50   25808 use Moo;
  50         323172  
  50         209  
12 50     50   61782 use strictures 2;
  50         390  
  50         1738  
13 50     50   17005 use stable 0.031 'postderef';
  50         720  
  50         354  
14 50     50   8210 use experimental 'signatures';
  50         154  
  50         203  
15 50     50   2365 no autovivification warn => qw(fetch store exists delete);
  50         130  
  50         332  
16 50     49   3505 use if "$]" >= 5.022, experimental => 're_strict';
  49         91  
  49         1150  
17 49     49   3323 no if "$]" >= 5.031009, feature => 'indirect';
  49         104  
  49         2797  
18 49     49   236 no if "$]" >= 5.033001, feature => 'multidimensional';
  49         92  
  49         2053  
19 49     49   270 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  49         114  
  49         2115  
20 49     49   243 no if "$]" >= 5.041009, feature => 'smartmatch';
  49         99  
  49         1701  
21 49     49   338 no feature 'switch';
  49         236  
  49         1172  
22 49     49   21503 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  49         8034694  
  49         1967  
23 49     49   462 use Carp qw(croak carp);
  49         243  
  49         3562  
24 49     48   316 use List::Util 1.55 qw(pairs first uniqint pairmap uniqstr min);
  48         1225  
  47         4334  
25 47     48   294 use if "$]" < 5.041010, 'List::Util' => 'any';
  48         550  
  48         2087  
26 48     48   215 use if "$]" >= 5.041010, experimental => 'keyword_any';
  48         468  
  47         958  
27 47     48   24651 use builtin::compat qw(refaddr load_module);
  48         716514  
  48         2593  
28 48     48   27431 use Mojo::URL;
  48         383546  
  48         2776  
29 48     48   23215 use Safe::Isa;
  48         24625  
  48         8905  
30 48     48   21189 use Mojo::File 'path';
  48         727472  
  48         5620  
31 48     47   436 use Storable 'dclone';
  47         111  
  47         3067  
32 47     47   233 use File::ShareDir 'dist_dir';
  47         83  
  47         2664  
33 47     47   20950 use MooX::TypeTiny 0.002002;
  47         16567  
  47         258  
34 47     47   340267 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  47         4889879  
  47         690  
35 47     47   186621 use Digest::MD5 'md5';
  47         101  
  47         2882  
36 47     47   25874 use Feature::Compat::Try;
  47         16264  
  47         378  
37 47     47   23511 use JSON::Schema::Modern::Error;
  47         884  
  47         1949  
38 47     47   24255 use JSON::Schema::Modern::Result;
  47         528  
  47         1899  
39 47     47   24709 use JSON::Schema::Modern::Document;
  47         894  
  47         812  
40 47     47   3050 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);
  47         89  
  47         5083  
41 47     47   308 use namespace::clean;
  47         98  
  47         451  
42              
43             our @CARP_NOT = qw(
44             JSON::Schema::Modern::Document
45             JSON::Schema::Modern::Vocabulary
46             JSON::Schema::Modern::Vocabulary::Applicator
47             JSON::Schema::Modern::Document::OpenAPI
48             OpenAPI::Modern
49             );
50              
51 47     47   29724 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  47         126  
  47         3607  
52 47     47   222 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  47         87  
  47         30809  
53              
54             has specification_version => (
55             is => 'ro',
56             isa => Enum(SPECIFICATION_VERSIONS_SUPPORTED),
57             coerce => sub {
58             return $_[0] if any { $_[0] eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*;
59             my $real = 'draft'.($_[0]//'');
60             (any { $real eq $_ } SPECIFICATION_VERSIONS_SUPPORTED->@*) ? $real : $_[0];
61             },
62             );
63              
64             has output_format => (
65             is => 'ro',
66             isa => Enum(JSON::Schema::Modern::Result->OUTPUT_FORMATS),
67             default => 'basic',
68             );
69              
70             has short_circuit => (
71             is => 'ro',
72             isa => Bool,
73             lazy => 1,
74             default => sub { $_[0]->output_format eq 'flag' && !$_[0]->collect_annotations },
75             );
76              
77             has max_traversal_depth => (
78             is => 'ro',
79             isa => Int,
80             default => 50,
81             );
82              
83             has validate_formats => (
84             is => 'ro',
85             isa => Bool,
86             lazy => 1,
87             # as specified by https://json-schema.org/draft//schema#/$vocabulary
88             default => sub { ($_[0]->specification_version//SPECIFICATION_VERSION_DEFAULT) =~ /^draft[467]\z/ ? 1 : 0 },
89             );
90              
91             has validate_content_schemas => (
92             is => 'ro',
93             isa => Bool,
94             lazy => 1,
95             # defaults to false in latest versions, as specified by
96             # https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.8.2
97             default => sub { ($_[0]->specification_version//'') eq 'draft7' },
98             );
99              
100             has [qw(collect_annotations scalarref_booleans stringy_numbers strict with_defaults)] => (
101             is => 'ro',
102             isa => Bool,
103             );
104              
105              
106             # { $format_name => { type => ..., sub => ... }, ... }
107             has _format_validations => (
108             is => 'bare',
109             isa => my $format_type = HashRef[Dict[
110             type => core_types_type|ArrayRef[core_types_type],
111             sub => CodeRef,
112             ]],
113             init_arg => 'format_validations',
114             );
115              
116 969   100 969   1311 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  969         1201  
  969         1169  
  969         1125  
  969         7896  
117              
118 12     12 1 24890 sub add_format_validation ($self, $format, $definition) {
  12         27  
  12         22  
  12         17  
  12         19  
119 12 50 100     83 return if exists(($self->{_format_validations}//{})->{$format});
120              
121 12 100       41 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
122 12         99 $format_type->({ $format => $definition });
123              
124             # all core formats are of type string (so far); changing type of custom format is permitted
125             croak "Type for override of format $format does not match original type"
126 5 100 100     259 if core_formats_type->check($format) and $definition->{type} ne 'string';
127              
128 47     47   380 use autovivification 'store';
  47         111  
  47         419  
129 4         2373 $self->{_format_validations}{$format} = $definition;
130             }
131              
132             around BUILDARGS => sub ($orig, $class, @args) {
133             my $args = $class->$orig(@args);
134             croak 'output_format: strict_basic can only be used with specification_version: draft2019-09'
135             if ($args->{output_format}//'') eq 'strict_basic'
136             and ($args->{specification_version}//'') ne 'draft2019-09';
137              
138             croak 'collect_annotations cannot be used with specification_version '.$args->{specification_version}
139             if $args->{collect_annotations} and ($args->{specification_version}//'') =~ /^draft[467]\z/;
140              
141             $args->{format_validations} = +{
142             map +($_->[0] => ref $_->[1] eq 'HASH' ? $_->[1] : +{ type => 'string', sub => $_->[1] }),
143             pairs $args->{format_validations}->%*
144             } if $args->{format_validations};
145              
146             return $args;
147             };
148              
149             sub add_schema {
150 17395 50   17395 1 102069 croak 'insufficient arguments' if @_ < 2;
151 17395         26609 my $self = shift;
152              
153 17395 100       67626 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
154 2         216 Carp::carp('use of deprecated form of add_schema with document');
155 2         11 return $self->add_document($_[0]);
156             }
157              
158             # TODO: resolve $uri against $self->base_uri
159 17394 50       170585 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
160             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
161              
162 17394 100       253890 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
163 17393 50       92240 croak 'insufficient arguments' if not @_;
164              
165 17393 100       40044 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
166 2         307 Carp::carp('use of deprecated form of add_schema with document');
167 2         14 return $self->add_document($uri, $_[0]);
168             }
169              
170             # document BUILD will trigger $self->traverse($schema)
171             # Note we do not pass the uri to the document constructor, so resources in that document may still
172             # be relative
173 17392         505905 my $document = JSON::Schema::Modern::Document->new(
174             schema => $_[0],
175             evaluator => $self, # used mainly for traversal during document construction
176             );
177              
178             # try to reuse the same document, if the same schema is being added twice:
179             # this results in _add_resource silently ignoring the duplicate add, rather than erroring.
180 17392         296005 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
181 17392 100       885792 if (my $existing_doc = first {
182 744689   66 744689   8041094 my $existing_checksum = $_->_checksum
183             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
184 744689 100       3487568 $existing_checksum eq $schema_checksum
185             and $_->canonical_uri eq $document->canonical_uri
186             # FIXME: must also check spec version/metaschema_uri/vocabularies
187             } uniqint map $_->{document}, $self->_canonical_resources) {
188 12303         2720341 $document = $existing_doc;
189             }
190              
191 17392         472920 $self->add_document($uri, $document);
192             }
193              
194             sub add_document {
195 18204 50   18204 1 107729 croak 'insufficient arguments' if @_ < 2;
196 18204         33874 my $self = shift;
197              
198             # TODO: resolve $uri against $self->base_uri
199 18204 50       64813 my $base_uri = !$_[0]->$_isa('JSON::Schema::Modern::Document') ? Mojo::URL->new(shift)
    100          
200             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
201              
202 18204 50       5044619 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
203 18204 50       94840 croak 'insufficient arguments' if not @_;
204              
205 18204         29514 my $document = shift;
206 18204 50       47213 croak 'wrong document type' if not $document->$_isa('JSON::Schema::Modern::Document');
207              
208             # we will never add a document to the resource index if it has errors
209 18204 100       220662 die JSON::Schema::Modern::Result->new(
210             output_format => $self->output_format,
211             valid => 0,
212             errors => [ $document->errors ],
213             exception => 1,
214             ) if $document->has_errors;
215              
216 18024 100       49737 if (not length $base_uri){
217 17191         1637032 foreach my $res_pair ($document->resource_pairs) {
218 18051         208290 my ($uri_string, $doc_resource) = @$res_pair;
219              
220             # this might croak if there are duplicates or malformed entries.
221 18051         130703 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
222             }
223              
224 17186         5929856 return $document;
225             }
226              
227 834         122072 my @root; # uri_string => resource hash of the resource at path ''
228              
229             # document resources are added after resolving each resource against our provided base uri
230 834         2453 foreach my $res_pair ($document->resource_pairs) {
231 864         1923 my ($uri_string, $doc_resource) = @$res_pair;
232 864         2232 $uri_string = Mojo::URL->new($uri_string)->to_abs($base_uri)->to_string;
233              
234             my $new_resource = {
235             canonical_uri => Mojo::URL->new($doc_resource->{canonical_uri})->to_abs($base_uri),
236 864         379750 $doc_resource->%{qw(path specification_version vocabularies)},
237             document => $document,
238             };
239              
240 864   100     312068 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
241 47     47   51526 use autovivification 'store';
  47         105  
  47         217  
242             $new_resource->{anchors}{$anchor} = {
243             $doc_resource->{anchors}{$anchor}->%{path},
244             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
245 170 100       1622 canonical_uri => Mojo::URL->new($doc_resource->{anchors}{$anchor}{canonical_uri})->to_abs($base_uri),
246             };
247             }
248              
249             # this might croak if there are duplicates or malformed entries.
250 864         63081 $self->_add_resource($uri_string => $new_resource);
251 857 100 66     298172 @root = ($uri_string => $new_resource) if $new_resource->{path} eq '' and $uri_string !~ /#./;
252             }
253              
254             # associate the root resource with the base uri we were provided, if it does not already exist
255 827 100       3389 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
256              
257 827         131162 return $document;
258             }
259              
260 4     4 1 3541 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         16  
  4         11  
  4         15  
  4         10  
  4         7  
261 4 50       20 croak 'evaluate_json_string called in void context' if not defined wantarray;
262              
263 4         9 my $data;
264 4         10 try {
265 4         102 $data = $self->_json_decoder->decode($json_data)
266             }
267             catch ($e) {
268 3         81 return JSON::Schema::Modern::Result->new(
269             output_format => $self->output_format,
270             valid => 0,
271             exception => 1,
272             errors => [
273             JSON::Schema::Modern::Error->new(
274             depth => 0,
275             mode => 'traverse',
276             keyword => undef,
277             keyword_location => '',
278             error => $e,
279             )
280             ],
281             );
282             }
283              
284 2         16 return $self->evaluate($data, $schema, $config_override);
285             }
286              
287             # this is called whenever we need to walk a document for something.
288             # for now it is just called when a ::Document object is created, to verify the integrity of the
289             # schema structure, to identify the metaschema (via the $schema keyword), and to extract all
290             # embedded resources via $id and $anchor keywords within.
291             # Returns the internal $state object accumulated during the traversal.
292 17944     17944 1 61962 sub traverse ($self, $schema_reference, $config_override = {}) {
  17944         26365  
  17944         26143  
  17944         25288  
  17944         22438  
293 17944         51651 my %overrides = %$config_override;
294 17944         61882 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
295 17944 50       42434 croak join(', ', sort keys %overrides), ' not supported as a config override in traverse'
296             if keys %overrides;
297              
298             # Note: the starting position is not guaranteed to be at the root of the $document,
299             # nor is the fragment portion of this uri necessarily empty
300 17944   66     83881 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
301 17944   100     5306190 my $initial_path = $config_override->{traversed_keyword_path} // '';
302 17944   100     129160 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
303              
304 17944 50       70873 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
305              
306 17944 100       46383 if (length(my $uri_path = $initial_uri->fragment)) {
307 5 50       27 croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
308              
309 5 50       29 croak 'traversed_keyword_path does not match initial_schema_uri path fragment'
310             if substr($initial_path, -length($uri_path)) ne $uri_path;
311             }
312              
313             my $state = {
314             depth => 0,
315             data_path => '', # this never changes since we don't have an instance yet
316             initial_schema_uri => $initial_uri, # the canonical URI as of the start of this method or last $id
317             traversed_keyword_path => $initial_path, # the accumulated traversal path as of the start or last $id
318             keyword_path => '', # the rest of the path, since the start of this method or last $id
319             specification_version => $spec_version,
320             errors => [],
321             identifiers => {},
322             subschemas => [],
323             $config_override->{skip_ref_checks} ? () : (references => []),
324             callbacks => $config_override->{callbacks} // {},
325 17944 100 100     294533 evaluator => $self,
326             traverse => 1,
327             };
328              
329 17944         39785 my $valid = 1;
330              
331 17944         32008 try {
332             # determine the initial value of specification_version and vocabularies, so we have something to start
333             # with in _traverse_subschema().
334             # a subsequent "$schema" keyword can still change these values, and it is always processed
335             # first, so the override is skipped if the keyword exists in the schema
336             $state->{metaschema_uri} =
337             (ref $schema_reference eq 'HASH' && exists $schema_reference->{'$schema'} ? undef
338 17944 100 100     198653 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
339              
340 17944 100       67101 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
341 17938         414307 $state->@{qw(specification_version vocabularies)} = @$metaschema_info;
342             }
343             else {
344             # metaschema has not been processed for vocabularies yet...
345              
346             die 'something went wrong - cannot get metaschema data for '.$state->{metaschema_uri}
347 7 50       1011 if not $config_override->{metaschema_uri};
348              
349             # use the Core vocabulary to set metaschema info via the '$schema' keyword implementation
350             $valid = $self->_get_metaschema_vocabulary_classes($self->METASCHEMA_URIS->{$spec_version})->[1][0]
351 7         31 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
352             }
353              
354 17944 100 66     134314 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
355 17944 50 66     43223 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
356 17944 50 66     113318 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
357             }
358             catch ($e) {
359 1 0       1 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    0          
360 1         54 push $state->{errors}->@*, $e->errors;
361             }
362             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
363             # note: we should never be here, since traversal subs are no longer fatal
364 1         8 push $state->{errors}->@*, $e;
365             }
366             else {
367 1         3 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
368             }
369             }
370              
371 17944         73817 return $state;
372             }
373              
374             # the actual runtime evaluation of the schema against input data.
375 17391     17391 1 22498159 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17391         31645  
  17391         30862  
  17391         25921  
  17391         32429  
  17391         30574  
376 17391 50       47673 croak 'evaluate called in void context' if not defined wantarray;
377              
378 17391         42586 my %overrides = %$config_override;
379 17391         57939 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)};
380 17391 50       41631 croak join(', ', sort keys %overrides), ' not supported as a config override in evaluate'
381             if keys %overrides;
382              
383             my $state = {
384             data_path => $config_override->{data_path} // '',
385 17391   100     166961 traversed_keyword_path => $config_override->{traversed_keyword_path} // '', # the accumulated path as of the start of evaluation or last $id or $ref
      100        
386             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start of evaluation or last $id or $ref
387             keyword_path => '', # the rest of the path, since the start of evaluation or last $id or $ref
388             errors => [],
389             depth => 0,
390             };
391              
392 17391 100       1195263 $state->{data} = jsonp_set('', $state->{data_path}, ref $data ? dclone($data) : $data);
393              
394 17391         36083 my $valid;
395 17391         29576 try {
396 17391 100 100     53360 if (is_schema($schema_reference)) {
    100          
397             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
398 17315         56680 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
399             }
400             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
401 4         37 abort($state, 'invalid schema type: %s', get_type($schema_reference));
402             }
403              
404 17201         161084 my $schema_info = $self->_fetch_from_uri($schema_reference);
405 17201 100       40014 abort($state, 'EXCEPTION: unable to find resource "%s"', $schema_reference)
406             if not $schema_info;
407              
408             abort($state, 'EXCEPTION: "%s" is not a schema', $schema_reference)
409 17195 100       70866 if not $schema_info->{document}->get_entity_at_location($schema_info->{document_path});
410              
411             $state = +{
412             %$state,
413             initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
414             $schema_info->%{qw(document specification_version vocabularies)},
415             dynamic_scope => [ $schema_info->{canonical_uri}->clone->fragment(undef) ],
416             annotations => [],
417             seen => {},
418             callbacks => $config_override->{callbacks} // {},
419             evaluator => $self,
420             (map {
421 120358   100     2012930 my $val = $config_override->{$_} // $self->$_;
422 120358 100       775160 defined $val ? ($_ => $val) : ()
423             # note: this is a subset of the allowed overrides defined above
424             } qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
425 17194 100 100     124682 $config_override->{with_defaults} // $self->with_defaults ? (defaults => {}) : (),
      100        
426             };
427              
428             # this hash will be added to at each level of schema evaluation
429 17194 100       69585 $state->{seen_data_properties} = {} if $config_override->{_strict_schema_data};
430              
431             # we're going to set collect_annotations during evaluation when we see an unevaluated* keyword
432             # (or for object data when the _strict_schema_data configuration is set),
433             # but after we pass to a new data scope we'll clear it again.. unless we've got the config set
434             # globally for the entire evaluation, so we store that value in a high bit.
435 17194   100     63337 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
436              
437 17194         74793 $valid = $self->_evaluate_subschema($data, $schema_info->{schema}, $state);
438 17171 50 66     63024 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
439 17171 50 66     113159 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
440             }
441             catch ($e) {
442 219 100       1090 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
443 180         3040 return $e;
444             }
445             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
446 34         732 push $state->{errors}->@*, $e;
447             }
448             else {
449 5         111 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
450             }
451             }
452              
453 17210 100       49859 if ($state->{seen_data_properties}) {
454 5         10 my %unknown_keywords;
455 5         52 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
456             keys $state->{seen_data_properties}->%*) {
457 15         48 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
458 15   100     53 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
459             }
460              
461 5         20 foreach my $parent (sort keys %unknown_keywords) {
462             $valid = E({ %$state, data_path => $parent },
463             'unknown keyword%s seen in schema: %s', $unknown_keywords{$parent}->@* > 1 ? 's' : '',
464 8 100       130 join(', ', sort $unknown_keywords{$parent}->@*));
465             }
466             }
467              
468 17210 50 50     80960 die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
469              
470             return JSON::Schema::Modern::Result->new(
471             output_format => $self->output_format,
472             valid => $valid,
473             $valid
474             # strip annotations from result if user didn't explicitly ask for them
475             ? ($config_override->{collect_annotations} // $self->collect_annotations
476             ? (annotations => $state->{annotations}) : ())
477             : (errors => $state->{errors}),
478             $state->{defaults} ? (defaults => $state->{defaults}) : (),
479             data => $state->{data},
480 17210 100 100     535606 );
    100          
    100          
481             }
482              
483 10     11 1 34038 sub validate_schema ($self, $schema, $config_override = {}) {
  10         26  
  10         21  
  10         23  
  10         23  
484 10 50       49 croak 'validate_schema called in void context' if not defined wantarray;
485              
486             my $metaschema_uri = ref $schema eq 'HASH' && $schema->{'$schema'} ? $schema->{'$schema'}
487 10 100 66     140 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
488              
489             my $result = $self->evaluate($schema, $metaschema_uri,
490 10 100 100     116 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
491              
492 10 100       274 return $result if not $result->valid;
493              
494             # the traversal pass will validate all constraints that weren't handled by the metaschema
495 3         79 my $state = $self->traverse($schema);
496             return JSON::Schema::Modern::Result->new(
497             output_format => $self->output_format,
498             valid => 0,
499             errors => $state->{errors},
500 3 100       55 ) if $state->{errors}->@*;
501              
502 2         35 return $result; # valid: true
503             }
504              
505 8     9 1 38283 sub get ($self, $uri_reference) {
  8         16  
  8         17  
  8         13  
506 8 100       24 if (wantarray) {
507 5         20 my $schema_info = $self->_fetch_from_uri($uri_reference);
508 5 100       25 return if not $schema_info;
509 4 100       450 my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
510 4         40 return ($subschema, $schema_info->{canonical_uri});
511             }
512             else { # abridged version of _fetch_from_uri
513 3 50       25 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
514 3         416 my $fragment = $uri_reference->fragment;
515 3         16 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
516 3 50       115 return if not $resource;
517              
518 3         5 my $schema;
519 3 100 100     35 if (not length($fragment) or $fragment =~ m{^/}) {
520 2   100     23 $schema = $resource->{document}->get($resource->{path}.($fragment//''));
521             }
522             else { # we are following a URI with a plain-name fragment
523 1 50 50     17 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
524 0         0 $schema = $resource->{document}->get($subresource->{path});
525             }
526 2 100       233 return ref $schema ? dclone($schema) : $schema;
527             }
528             }
529              
530 0     1 1 0 sub get_document ($self, $uri_reference) {
  0         0  
  0         0  
  0         0  
531 0         0 my $schema_info = $self->_fetch_from_uri($uri_reference);
532 0 0       0 return if not $schema_info;
533 0         0 return $schema_info->{document};
534             }
535              
536             # defined lower down:
537             # sub add_media_type ($self, $media_type, $sub) { ... }
538             # sub get_media_type ($self, $media_type) { ... }
539             # sub add_encoding ($self, $encoding, $sub) { ... }
540             # sub get_encoding ($self, $encoding) { ... }
541             # sub add_vocabulary ($self, $classname) { ... }
542              
543             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
544              
545             # current spec version => { keyword => undef, or arrayref of alternatives }
546             my %removed_keywords = (
547             'draft4' => {
548             },
549             'draft6' => {
550             id => [ '$id' ],
551             },
552             'draft7' => {
553             id => [ '$id' ],
554             },
555             'draft2019-09' => {
556             id => [ '$id' ],
557             definitions => [ '$defs' ],
558             dependencies => [ qw(dependentSchemas dependentRequired) ],
559             },
560             'draft2020-12' => {
561             id => [ '$id' ],
562             definitions => [ '$defs' ],
563             dependencies => [ qw(dependentSchemas dependentRequired) ],
564             '$recursiveAnchor' => [ '$dynamicAnchor' ],
565             '$recursiveRef' => [ '$dynamicRef' ],
566             additionalItems => [ 'items' ],
567             },
568             );
569              
570             # {
571             # $spec_version => {
572             # $vocabulary_class => {
573             # traverse => [ [ $keyword => $subref ], [ ... ] ],
574             # evaluate => [ [ $keyword => $subref ], [ ... ] ],
575             # }
576             # }
577             # }
578             # If we could serialize coderefs, this could be an object attribute;
579             # otherwise, we might as well persist this for the lifetime of the process.
580             our $vocabulary_cache = {};
581              
582 42172     42173   59034 sub _traverse_subschema ($self, $schema, $state) {
  42172         54203  
  42172         49014  
  42172         48627  
  42172         48689  
583 42172         305947 delete $state->@{'keyword', grep /^_/, keys %$state};
584              
585             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
586 42172 50       185343 if $state->{depth}++ > $self->max_traversal_depth;
587              
588 42172         155958 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
589              
590 42172         119554 my $schema_type = get_type($schema);
591             return 1 if $schema_type eq 'boolean'
592             and ($state->{specification_version} ne 'draft4'
593 42172 100 100     138992 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
594              
595 34462 100       65204 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
596              
597 34445 100       92477 return 1 if not keys %$schema;
598              
599 33246         46256 my $valid = 1;
600 33246         182504 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
601              
602             # we use an index rather than iterating through the lists directly because the lists of
603             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
604             # and $schema keyword can make this change, and they both come first, therefore a simple index
605             # into the list is sufficient.
606             ALL_KEYWORDS:
607 33246         108157 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
608 203351         309213 my $vocabulary = $state->{vocabularies}[$vocab_index];
609 203351         212081 my $keyword_list;
610              
611 203351   66     317867 for (my $keyword_index = 0;
612             $keyword_index < ($keyword_list //= do {
613 47     47   157244 use autovivification qw(fetch store);
  47         134  
  47         250  
614             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{traverse} //= [
615             map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
616             $vocabulary->keywords($state->{specification_version})
617 203419   100     644944 ];
618             })->@*;
619             $keyword_index++) {
620 1684836         2357886 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
621 1684836 100       3711413 next if not exists $schema->{$keyword};
622              
623             # keywords adjacent to $ref are not evaluated before draft2019-09
624 56872 100 100     214161 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
625              
626 56836         100465 delete $unknown_keywords{$keyword};
627 56836         100173 $state->{keyword} = $keyword;
628              
629 56836         87863 my $old_spec_version = $state->{specification_version};
630 56836         95584 my $error_count = $state->{errors}->@*;
631              
632 56836 100       180907 if (not $sub->($vocabulary, $schema, $state)) {
633             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
634 243 50       785 if $error_count == $state->{errors}->@*;
635 243         357 $valid = 0;
636 243         879 next;
637             }
638             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
639 56593 50       177899 if $error_count != $state->{errors}->@*;
640              
641             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
642 56593 100       116896 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
643              
644 56593 100       224446 if (my $callback = $state->{callbacks}{$keyword}) {
645 4         11 $error_count = $state->{errors}->@*;
646              
647 4 50       14 if (not $callback->($schema, $state)) {
648             die 'callback result is false but there are no errors (keyword: '.$keyword.')'
649 0 0       0 if $error_count == $state->{errors}->@*;
650 0         0 $valid = 0;
651 0         0 next;
652             }
653             die 'callback result is true but there are errors (keyword: '.$keyword.')'
654 4 50       2416 if $error_count != $state->{errors}->@*;
655             }
656             }
657             }
658              
659 33246         71402 delete $state->{keyword};
660              
661 33246 100 100     96254 if ($self->strict and keys %unknown_keywords) {
662 2 50       13 $valid = E($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
663             join(', ', sort keys %unknown_keywords));
664             }
665              
666             # check for previously-supported but now removed keywords
667 33246         174261 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
668 104050 100       177451 next if not exists $schema->{$keyword};
669 224         910 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
670             .canonical_uri($state).'")';
671 224 50       26620 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
672 224         1103 my @list = map '"'.$_.'"', @$alternates;
673 224 50       589 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
674 224 100       763 splice(@list, -1, 0, 'or') if @list > 1;
675 224         685 $message .= ': this should be rewritten as '.join(' ', @list);
676             }
677 224         58666 carp $message;
678             }
679              
680 33246         161101 return $valid;
681             }
682              
683 34834     34835   49396 sub _evaluate_subschema ($self, $data, $schema, $state) {
  34834         43409  
  34834         45338  
  34834         44802  
  34834         41846  
  34834         43250  
684 34834 50       73224 croak '_evaluate_subschema called in void context' if not defined wantarray;
685              
686             # callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
687             # paths; but annotations, errors are arrayrefs so their contents will be shared
688 34834   100     135974 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
689 34834         295682 delete $state->@{'keyword', grep /^_/, keys %$state};
690              
691             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
692 34834 100       147826 if $state->{depth}++ > $self->max_traversal_depth;
693              
694 34831         106847 my $schema_type = get_type($schema);
695 34831 100 66     78556 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
696              
697             # this should never happen, due to checks in traverse
698 33987 50       64881 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
699              
700 33987 100       77840 return 1 if not keys %$schema;
701              
702             # find all schema locations in effect at this data path + uri combination
703             # if any of them are absolute prefix of this schema location, we are in a loop.
704 33349         92475 my $canonical_uri = canonical_uri($state);
705 33349         87941 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
706             {
707 47     47   52663 use autovivification qw(fetch store);
  47         99  
  47         260  
  33349         42170  
708             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
709             if grep substr($schema_location, 0, length) eq $_,
710 33349 100       132347 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
711 33348         4297940 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
712             }
713              
714 33348         3424526 my $valid = 1;
715 33348         230647 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
716              
717             # set aside annotations collected so far; they are not used in the current scope's evaluation
718 33348         77116 my $parent_annotations = $state->{annotations};
719 33348         67548 $state->{annotations} = [];
720              
721             # in order to collect annotations from applicator keywords only when needed, we twiddle the low
722             # bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
723             # We also set it when _strict_schema_data is set, but only for object data instances.
724             $state->{collect_annotations} |=
725             0+((ref $data eq 'ARRAY' && exists $schema->{unevaluatedItems})
726             || ((my $is_object_data = ref $data eq 'HASH')
727 33348   100     260831 && (exists $schema->{unevaluatedProperties} || !!$state->{seen_data_properties})));
728              
729             # set aside defaults collected so far; we need to keep the subschema's defaults separated in
730             # case they must be discarded due to overall invalidity of the subschema
731 33348         64583 my $defaults = $state->{defaults};
732 33348 100       65674 $state->{defaults} = {} if $state->{defaults};
733              
734             # we use an index rather than iterating through the lists directly because the lists of
735             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
736             # and $schema keyword can make this change, and they both come first, therefore a simple index
737             # into the list is sufficient.
738              
739             ALL_KEYWORDS:
740 33348         103535 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
741 183051         290977 my $vocabulary = $state->{vocabularies}[$vocab_index];
742 183051         192908 my $keyword_list;
743              
744 183051   66     288377 for (my $keyword_index = 0;
745             $keyword_index < ($keyword_list //= do {
746 47     47   16301 use autovivification qw(fetch store);
  47         101  
  47         249  
747             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{evaluate} //= [
748             map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
749             $vocabulary->keywords($state->{specification_version})
750 183054   100     626470 ];
751             })->@*;
752             $keyword_index++) {
753 1510156         2127920 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
754 1510156 100       3266476 next if not exists $schema->{$keyword};
755              
756             # keywords adjacent to $ref are not evaluated before draft2019-09
757 60037 100 100     217590 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
758              
759 60006         103946 delete $unknown_keywords{$keyword};
760 60006 100 100     122444 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
761              
762 60005         117832 $state->{keyword} = $keyword;
763              
764 60005 100       93417 if ($sub) {
765 55929         87769 my $old_spec_version = $state->{specification_version};
766 55929         96808 my $error_count = $state->{errors}->@*;
767              
768 55929         83842 try {
769 55929 100       184592 if (not $sub->($vocabulary, $data, $schema, $state)) {
770             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
771 13161 50       42126 if $error_count == $state->{errors}->@*;
772 13161         18509 $valid = 0;
773              
774 13161 100 100     62763 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
775 6948         31246 next;
776             }
777              
778             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
779 42724 50       296762 if $error_count != $state->{errors}->@*;
780             }
781             catch ($e) {
782 44 100       808 die $e if $e->$_isa('JSON::Schema::Modern::Error');
783 2         25 abort($state, 'EXCEPTION: '.$e);
784             }
785              
786             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
787 42724 100       101148 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
788             }
789              
790 46800 100 100     194106 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
791 24         41 my $error_count = $state->{errors}->@*;
792              
793 24 100       66 if (not $callback->($data, $schema, $state)) {
794             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
795 2 50       9 if $error_count == $state->{errors}->@*;
796 2         4 $valid = 0;
797              
798 2 100 66     10 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
799 1         4 next;
800             }
801             warn 'callback result is true but there are errors (keyword: '.$keyword.')'
802 22 50       581 if $error_count != $state->{errors}->@*;
803             }
804             }
805             }
806              
807 33304         80556 delete $state->{keyword};
808              
809 33304 100 100     70773 if ($state->{strict} and keys %unknown_keywords) {
810 3 100       22 abort($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
811             join(', ', sort keys %unknown_keywords));
812             }
813              
814             # Note: we can remove all of this entirely and just rely on strict mode when we (eventually!) remove
815             # the traverse phase and replace with evaluate-against-metaschema.
816 33301 100 100     83032 if ($state->{seen_data_properties} and $is_object_data) {
817             # record the locations of all local properties
818             $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 0
819 156         1097 foreach grep !/^x-/, keys %$data;
820              
821             my @evaluated_properties = map {
822 156         431 my $keyword = $_->{keyword};
  577         741  
823             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
824 577 100       1335 ? $_->{annotation}->@* : ();
825             } local_annotations($state);
826              
827             # tick off properties that were recognized by this subschema
828 156         354 $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 1 foreach @evaluated_properties;
829              
830             # weird! the draft4 metaschema doesn't know about '$ref' at all!
831             $state->{seen_data_properties}{$state->{data_path}.'/$ref'} |= 1
832 156 100 66     447 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
833             }
834              
835 33301 100 100     108770 if ($valid and $state->{collect_annotations} and $state->{specification_version} !~ /^draft(?:[467]|2019-09)\z/) {
      100        
836             annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
837 969         3268 foreach sort keys %unknown_keywords;
838             }
839              
840             # only keep new annotations if schema is valid
841 33301 100       80876 push $parent_annotations->@*, $state->{annotations}->@* if $valid;
842              
843             # only keep new defaults if schema is valid
844             $defaults->@{keys $state->{defaults}->%*} = values $state->{defaults}->%*
845 33301 100 100     84724 if $valid and $state->{defaults};
846              
847 33301         245674 return $valid;
848             }
849              
850             has _resource_index => (
851             is => 'bare',
852             isa => Map[my $resource_key_type = Str->where('!/#/'), my $resource_type = Dict[
853             canonical_uri => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
854             path => json_pointer_type, # JSON pointer relative to the document root
855             specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
856             document => InstanceOf['JSON::Schema::Modern::Document'],
857             # the vocabularies used when evaluating instance data against schema
858             vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
859             anchors => Optional[HashRef[Dict[
860             canonical_uri => canonical_uri_type, # equivalent uri with json pointer fragment
861             path => json_pointer_type, # JSON pointer relative to the document root
862             dynamic => Optional[Bool],
863             ]]],
864             Slurpy[HashRef[Undef]], # no other fields allowed
865             ]],
866             );
867              
868             sub _get_resource {
869 44036 50   44037   113011 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
870 44036   100     3064083 ($_[0]->{_resource_index}//{})->{$_[1]}
871             }
872              
873             # does not check for duplicate entries, or for malformed uris
874             sub _add_resources_unsafe {
875 47     47   84559 use autovivification 'store';
  47         1205  
  47         577  
876             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
877 103     104   1131 foreach pairs @_[1..$#_];
878             }
879 25   50 26   12108 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
880 17395   100 17396   2983882 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
881 2123   50 2124   242676 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
882              
883 18977     18978   47639 sub _add_resource ($self, @kvs) {
  18977         29021  
  18977         37061  
  18977         27034  
884 18977         86500 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
885 18977         39823 my ($canonical_uri, $resource) = @$pair;
886              
887 18977 100       55656 if (my $existing = $self->_get_resource($canonical_uri)) {
    100          
888             # we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
889             # lack all identifiers altogether, but preserve other resources from the original document
890 17056 100       56881 if ($canonical_uri ne '') {
891             my @diffs = (
892             ($existing->{path} eq $resource->{path} ? () : 'path'),
893             ($existing->{canonical_uri} eq $resource->{canonical_uri} ? () : 'canonical_uri'),
894             ($existing->{specification_version} eq $resource->{specification_version} ? () : 'specification_version'),
895 859 100       5959 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
896 859 100       261660 next if not @diffs;
897 10         3235 croak 'uri "'.$canonical_uri.'" conflicts with an existing schema resource: documents differ by ',
898             join(', ', @diffs);
899             }
900             }
901             elsif (JSON::Schema::Modern::Utilities::get_schema_filename($canonical_uri)) {
902 2         569 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
903             }
904              
905 47     47   24747 use autovivification 'store';
  47         94  
  47         223  
906 18116         82758 $self->{_resource_index}{$resource_key_type->($canonical_uri)} = $resource_type->($resource);
907             }
908             }
909              
910             # $vocabulary uri (not its $id!) => [ specification_version, class ]
911             has _vocabulary_classes => (
912             is => 'bare',
913             isa => HashRef[
914             my $vocabulary_type = Tuple[
915             $spec_version_type,
916             $vocabulary_class_type,
917             ]
918             ],
919             reader => '__vocabulary_classes',
920             lazy => 1,
921             default => sub {
922             +{
923 12     13   789 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   23  
  12     13   189  
  12     12   435  
  12     12   19  
  12     12   160  
  12     12   522  
  12     12   20  
  12     7   179  
  12     7   694  
  12     7   27  
  12     7   165  
  12     7   583  
  12     7   24  
  12     7   170  
  12     7   536  
  12     1   19  
  12     1   148  
  12     1   45  
  12     1   259  
  12     1   128  
  12     1   39  
  12         21  
  12         36  
  7         43  
  7         11  
  7         156  
  7         25  
  7         12  
  7         89  
  7         26  
  7         13  
  7         69  
  7         26  
  7         11  
  7         79  
  7         25  
  7         13  
  7         94  
  7         24  
  7         11  
  7         73  
  7         25  
  7         13  
  7         61  
  7         31  
  7         11  
  7         36  
  1         3  
  1         1  
  1         25  
  1         4  
  1         1  
  1         10  
  1         3  
  1         3  
  1         8  
  1         3  
  1         1  
  1         9  
  1         3  
  1         1  
  1         10  
  1         3  
  1         2  
  1         3  
924             map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
925             qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
926             }
927             },
928             );
929              
930 107     108   2090 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
931              
932 10     11 1 19495 sub add_vocabulary ($self, $classname) {
  10         15  
  10         20  
  10         15  
933 10 50       233 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
934              
935 10         803 $vocabulary_class_type->(load_module($classname));
936              
937             # uri => version, uri => version
938 7         531 foreach my $pair (pairs $classname->vocabulary) {
939 7         82 my ($uri_string, $spec_version) = @$pair;
940 7         29 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
941 6         7221 $spec_version_type->($spec_version);
942              
943 4 100       491 croak 'keywords starting with "$" are reserved for core and cannot be used'
944             if grep /^\$/, $classname->keywords;
945              
946 3         27 $self->{_vocabulary_classes}{$uri_string} = $vocabulary_type->([ $spec_version, $classname ]);
947             }
948             }
949              
950             # $schema uri => [ specification_version, [ vocab classes, in evaluation order ] ].
951             has _metaschema_vocabulary_classes => (
952             is => 'bare',
953             isa => HashRef[
954             my $mvc_type = Tuple[
955             $spec_version_type,
956             ArrayRef[$vocabulary_class_type],
957             ]
958             ],
959             reader => '__metaschema_vocabulary_classes',
960             lazy => 1,
961             default => sub {
962 38     38   20885 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   159  
  38     38   1272  
  38     38   17681  
  38     38   186  
  38     38   1313  
  38     38   20215  
  38     30   174  
  38     30   1267  
  38     30   19533  
  38     30   209  
  38     30   1150  
  38     30   18552  
  38     30   834  
  38     19   1419  
  38     19   16101  
  38     19   123  
  38     19   1085  
  38     19   447  
  38     19   65  
  38     19   164  
  30     17   195  
  30     16   74  
  30     16   446  
  30     16   126  
  30     16   53  
  30     16   398  
  30     16   119  
  30     13   65  
  30     13   619  
  30     13   107  
  30     13   70  
  30     13   341  
  30     13   123  
  30     13   53  
  30     12   350  
  30     12   113  
  30     12   51  
  30     12   379  
  30     12   100  
  30     12   46  
  30     12   127  
  19     11   204  
  19     11   41  
  19     11   314  
  19     11   96  
  19     11   32  
  19     11   227  
  19     11   74  
  19     10   35  
  19     10   219  
  19     10   62  
  19     9   40  
  19     9   204  
  19     9   80  
  19     9   34  
  19     9   186  
  19     9   81  
  19     9   31  
  19     9   241  
  19     9   76  
  19     9   34  
  19     9   87  
  17     9   112  
  17     9   34  
  17     9   291  
  16     9   64  
  16     9   32  
  16     9   170  
  16     9   77  
  16     8   30  
  16     8   164  
  16     8   74  
  16     8   32  
  16     8   272  
  16     8   76  
  16     8   28  
  16     6   165  
  16     6   79  
  16     6   27  
  16     6   808  
  16     6   69  
  16     6   27  
  16     6   55  
  13     6   86  
  13     6   27  
  13     6   206  
  13     6   53  
  13     6   27  
  13     6   124  
  13     6   69  
  13     6   24  
  13     6   136  
  13     6   40  
  13     5   29  
  13     5   131  
  13     5   77  
  13     5   36  
  13     5   150  
  13     5   51  
  13     5   22  
  13     5   114  
  13     5   58  
  13     5   21  
  13     5   61  
  12     6   81  
  12     6   26  
  12     6   242  
  12     6   68  
  12     6   27  
  12     6   106  
  12     6   47  
  12     6   19  
  12     4   139  
  12     4   39  
  12     4   28  
  12     4   109  
  12     4   61  
  12     4   26  
  12     4   181  
  12     4   65  
  12     4   25  
  12     4   96  
  12     4   50  
  12     4   19  
  12     4   37  
  11         76  
  11         21  
  11         138  
  11         48  
  11         18  
  11         139  
  11         52  
  11         22  
  11         103  
  11         36  
  11         27  
  11         134  
  11         53  
  11         17  
  11         116  
  11         61  
  11         26  
  11         117  
  11         52  
  11         22  
  11         36  
  10         116  
  10         24  
  10         152  
  10         56  
  10         18  
  10         96  
  10         46  
  10         21  
  10         83  
  10         30  
  9         16  
  9         127  
  9         52  
  9         12  
  9         95  
  9         32  
  9         15  
  9         63  
  9         32  
  9         15  
  9         30  
  9         66  
  9         18  
  9         119  
  9         58  
  9         17  
  9         94  
  9         60  
  9         17  
  9         120  
  9         33  
  9         18  
  9         121  
  9         42  
  9         15  
  9         80  
  9         47  
  9         17  
  9         84  
  9         65  
  9         15  
  9         32  
  9         67  
  9         19  
  9         121  
  9         34  
  9         15  
  9         90  
  9         41  
  9         14  
  9         118  
  9         27  
  9         20  
  9         102  
  9         36  
  9         15  
  9         75  
  9         35  
  9         30  
  9         119  
  9         42  
  9         18  
  9         28  
  8         58  
  8         16  
  8         133  
  8         41  
  8         18  
  8         86  
  8         54  
  8         14  
  8         73  
  8         25  
  8         17  
  8         96  
  8         37  
  8         18  
  8         103  
  8         34  
  8         19  
  8         102  
  8         37  
  8         14  
  8         30  
  6         76  
  6         12  
  6         79  
  6         26  
  6         11  
  6         80  
  6         31  
  6         11  
  6         55  
  6         18  
  6         14  
  6         50  
  6         30  
  6         12  
  6         132  
  6         30  
  6         12  
  6         81  
  6         27  
  6         12  
  6         21  
  6         55  
  6         14  
  6         91  
  6         24  
  6         13  
  6         54  
  6         29  
  6         12  
  6         72  
  6         23  
  6         29  
  6         51  
  6         42  
  6         14  
  6         80  
  6         30  
  6         14  
  6         48  
  6         24  
  6         12  
  6         20  
  6         499  
  6         3905  
  6         98  
  6         573  
  6         3770  
  6         54  
  6         441  
  6         3247  
  6         71  
  5         17  
  5         12  
  5         50  
  5         25  
  5         11  
  5         34  
  5         21  
  5         12  
  5         49  
  5         22  
  5         10  
  5         14  
  5         52  
  5         9  
  5         58  
  5         22  
  5         8  
  5         41  
  5         19  
  5         10  
  5         36  
  5         15  
  5         13  
  5         47  
  5         27  
  5         9  
  5         50  
  5         21  
  5         10  
  5         47  
  5         21  
  5         9  
  5         15  
  6         40  
  6         10  
  6         70  
  6         25  
  6         10  
  6         47  
  6         25  
  6         9  
  6         74  
  6         22  
  6         18  
  6         49  
  6         32  
  6         17  
  6         58  
  6         32  
  6         14  
  6         78  
  6         50  
  6         14  
  6         32  
  6         38  
  6         14  
  6         45  
  4         370  
  4         10  
  4         43  
  4         67  
  4         12  
  4         28  
  4         12  
  4         10  
  4         28  
  4         20  
  4         8  
  4         35  
  4         19  
  4         9  
  4         39  
  4         32  
  4         7  
  4         14  
  4         25  
  4         10  
  4         54  
  4         30  
  4         8  
  4         28  
  4         16  
  4         7  
  4         42  
  4         13  
  4         11  
  4         46  
  4         21  
  4         29  
  4         50  
  4         25  
  4         8  
  4         48  
  4         20  
  4         7  
  4         10  
963             qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated);
964             +{
965             'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
966             do { pop @modules; () }, # remove Unevaluated
967             'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', [ @modules ] ],
968             'http://json-schema.org/draft-07/schema' => [ 'draft7', [ @modules ] ],
969             do { splice @modules, 4, 1; () }, # remove Content
970             'http://json-schema.org/draft-06/schema' => [ 'draft6', \@modules ],
971             'http://json-schema.org/draft-04/schema' => [ 'draft4', \@modules ],
972             },
973             },
974             );
975              
976 30503     30504   669919 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
977 5924     5925   35674 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
978 4     5   225 sub __all_metaschema_vocabulary_classes { values $_[0]->__metaschema_vocabulary_classes->%* }
979              
980             # translate vocabulary URIs into classes, caching the results (if any)
981 53     54   114 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         92  
  53         89  
  53         71  
  53         75  
982 53 100       263 if (not exists $schema_info->{schema}{'$vocabulary'}) {
983             # "If "$vocabulary" is absent, an implementation MAY determine behavior based on the meta-schema
984             # if it is recognized from the URI value of the referring schema's "$schema" keyword."
985 2         9 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
986 2         6 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
987             }
988              
989 51         120 my $valid = 1;
990             # Core §8.1.2-6: "The "$vocabulary" keyword SHOULD be used in the root schema of any schema
991             # document intended for use as a meta-schema. It MUST NOT appear in subschemas."
992 51 100       205 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
993 51 100       211 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
994              
995 51 100       175 return (undef, []) if not $valid;
996              
997 49         84 my @vocabulary_classes;
998              
999 49         292 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
1000 105         319 my $class_info = $self->_get_vocabulary_class($uri);
1001             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" is not a known vocabulary', $uri), next
1002 105 100 100     10437 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1003              
1004 97 100       879 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1005              
1006 89         208 my ($spec_version, $class) = @$class_info;
1007             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" uses %s, but the metaschema itself uses %s',
1008             $uri, $spec_version, $schema_info->{specification_version}), next
1009 89 100       306 if $spec_version ne $schema_info->{specification_version};
1010              
1011 83         183 push @vocabulary_classes, $class;
1012             }
1013              
1014             @vocabulary_classes = sort {
1015 49 50       212 $a->evaluation_order <=> $b->evaluation_order
  49 50       204  
1016             || ($a->evaluation_order == 999 ? 0
1017             : ($valid = E($state, '%s and %s have a conflicting evaluation_order', sort $a, $b)))
1018             } @vocabulary_classes;
1019              
1020 49 100 100     200 $valid = E($state, 'the first vocabulary (by evaluation_order) must be Core')
1021             if ($vocabulary_classes[0]//'') ne 'JSON::Schema::Modern::Vocabulary::Core';
1022              
1023 49         84 my %seen_keyword;
1024 49         108 foreach my $class (@vocabulary_classes) {
1025 83         338 foreach my $keyword ($class->keywords($schema_info->{specification_version})) {
1026             $valid = E($state, '%s keyword "%s" conflicts with keyword of the same name from %s',
1027             $class, $keyword, $seen_keyword{$keyword})
1028 794 100       1137 if $seen_keyword{$keyword};
1029 794         1524 $seen_keyword{$keyword} = $class;
1030             }
1031             }
1032              
1033 49 100       643 return ($schema_info->{specification_version}, $valid ? \@vocabulary_classes : []);
1034             }
1035              
1036             # used for determining a default '$schema' keyword where there is none
1037             # these are also normalized as this is how we cache them
1038 47         6354 use constant METASCHEMA_URIS => {
1039             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
1040             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
1041             'draft7' => 'http://json-schema.org/draft-07/schema',
1042             'draft6' => 'http://json-schema.org/draft-06/schema',
1043             'draft4' => 'http://json-schema.org/draft-04/schema',
1044 47     47   70496 };
  47         106  
1045              
1046             # for internal use only. files are under share/
1047 47         50027 use constant _CACHED_METASCHEMAS => {
1048             'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
1049             'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
1050             'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
1051             'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
1052             'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
1053             'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
1054             'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
1055             'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
1056             'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
1057             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
1058              
1059             'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
1060             'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
1061             'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
1062             'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
1063             'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
1064             'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
1065             'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
1066             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
1067              
1068             # trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
1069             'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
1070             'http://json-schema.org/draft-06/schema' => 'draft6/schema.json',
1071             'http://json-schema.org/draft-04/schema' => 'draft4/schema.json',
1072 47     47   256 };
  47         85  
1073              
1074             # simple runtime-wide cache of metaschema document objects that are sourced from disk
1075             my $metaschema_cache = {};
1076              
1077             {
1078             my $share_dir = dist_dir('JSON-Schema-Modern');
1079             JSON::Schema::Modern::Utilities::register_schema($_, $share_dir.'/'._CACHED_METASCHEMAS->{$_})
1080             foreach keys _CACHED_METASCHEMAS->%*;
1081             }
1082              
1083             # returns the same as _get_resource
1084 24428     24429   1713206 sub _get_or_load_resource ($self, $uri) {
  24428         35895  
  24428         33739  
  24428         29663  
1085 24428         57546 my $resource = $self->_get_resource($uri);
1086 24428 100       2361956 return $resource if $resource;
1087              
1088 114 100       598 if (my $document = load_cached_document($self, $uri)) {
1089 103         454 return $self->_get_resource($uri);
1090             }
1091              
1092             # TODO:
1093             # - load from network or disk
1094              
1095 11         25 return;
1096             };
1097              
1098             # returns information necessary to use a schema found at a particular URI or uri-reference:
1099             # - schema: a schema (which may not be at a document root)
1100             # - canonical_uri: the canonical uri for that schema,
1101             # - document: the JSON::Schema::Modern::Document object that holds that schema
1102             # - document_path: the path relative to the document root for this schema
1103             # - specification_version: the specification version that applies to this schema
1104             # - vocabularies: the vocabularies to use when considering schema keywords
1105             # creates a Document and adds it to the resource index, if not already present.
1106 24190     24191   238600 sub _fetch_from_uri ($self, $uri_reference) {
  24190         34691  
  24190         35409  
  24190         31190  
1107 24190 100       57233 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
1108              
1109             # this is *a* resource that would contain our desired location, but may not be the closest one
1110 24190         93604 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1111 24190 100       140053 return if not $resource;
1112              
1113 24179         54739 my $fragment = $uri_reference->fragment;
1114 24179 100 100     128239 if (not length($fragment) or $fragment =~ m{^/}) {
1115 23599   100     195833 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1116 23599 100       188821 return if not defined $subschema;
1117              
1118 23597         32415 my $closest_resource;
1119 23597 100       47885 if (not length $fragment) { # we already have the canonical resource root
1120 21474         45241 $closest_resource = [ undef, $resource ];
1121             }
1122             else {
1123             # determine the canonical uri by finding the closest schema resource(s)
1124 2123         5848 my $doc_addr = refaddr($resource->{document});
1125             my @closest_resources =
1126 504         2561 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1127             grep { !length($_->[1]{path}) # document root
1128 2913 100 66     25684 || length($document_path)
1129             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1130 2123         7353 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228352         373812  
1131             $self->_resource_pairs;
1132              
1133             # now whittle down to all the resources with the same document path as the first candidate
1134 2123 100       39695 if (@closest_resources > 1) {
1135             # find the resource key that most closely matches the original query uri, by matching prefixes
1136 464         2293 my $match = $uri_reference.'';
1137             @closest_resources =
1138 26         100 sort { _prefix_match_length($b->[0], $match) <=> _prefix_match_length($a->[0], $match) }
1139             grep $_->[1]{path} eq $closest_resources[0]->[1]{path},
1140 464         95502 @closest_resources;
1141             }
1142              
1143 2123         4892 $closest_resource = $closest_resources[0];
1144             }
1145              
1146             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1147 23597         93274 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1148 23597 100       1501907 $canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
1149              
1150             return {
1151             schema => $subschema,
1152             canonical_uri => $canonical_uri,
1153             document_path => $document_path,
1154 23597         326081 $closest_resource->[1]->%{qw(document specification_version vocabularies)}, # reference, not copy
1155             };
1156             }
1157             else { # we are following a URI with a plain-name fragment
1158 580 100 100     3862 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
1159             return {
1160             schema => $resource->{document}->get($subresource->{path}),
1161             canonical_uri => $subresource->{canonical_uri}, # this is *not* the anchor-containing URI
1162             document_path => $subresource->{path},
1163 578         3318 $resource->%{qw(document specification_version vocabularies)}, # reference, not copy
1164             };
1165             }
1166             }
1167              
1168             # given two strings, determines the number of characters in common, starting from the first
1169             # character
1170 52     53   80 sub _prefix_match_length ($x, $y) {
  52         77  
  52         71  
  52         61  
1171 52         134 my $len = min(length($x), length($y));
1172 52         120 foreach my $pos (0..$len) {
1173 1456 100       2438 return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
1174             }
1175 0         0 return $len;
1176             }
1177              
1178 47     47   347 use constant _JSON_BACKEND => JSON::Schema::Modern::Utilities::_JSON_BACKEND;
  47         98  
  47         50379  
1179              
1180             # used for internal encoding as well (when caching serialized schemas)
1181             has _json_decoder => (
1182             is => 'ro',
1183             isa => HasMethods[qw(encode decode)],
1184             lazy => 1,
1185             default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
1186             );
1187              
1188             # since media types are case-insensitive, all legacy type names must be casefolded on insertion.
1189             has _media_type => (
1190             is => 'bare',
1191             isa => ArrayRef[my $media_type_type = Str->where(q{$_ eq CORE::fc($_)})],
1192             reader => '__media_type',
1193             lazy => 1,
1194             default => sub ($self) { [] },
1195             );
1196              
1197             my ($warn_add_media_type, $warn_get_media_type); # we will warn just once
1198              
1199             # deprecated interface
1200 4     5 1 2738 sub add_media_type ($self, $media_type, $decoder) {
  4         34  
  4         7  
  4         6  
  4         3  
1201 4         52 $media_type_type->($media_type);
1202              
1203 4 100       1313 carp '$jsm->add_media_type is deprecated; use the function in JSON::Schema::Modern::Utilities instead' if not $warn_add_media_type++;
1204              
1205             # backcompat preservation: add to the global registry, and remove it again when this object goes
1206             # out of scope
1207 4         21 JSON::Schema::Modern::Utilities::add_media_type($media_type, $decoder, undef, refaddr $self);
1208 4         109 push $self->__media_type->@*, $media_type;
1209 4         47 return;
1210             }
1211              
1212 245     246   8826904 sub DESTROY ($self) {
  245         704  
  245         394  
1213 245         6338 foreach my $media_type (uniqstr $self->__media_type->@*) {
1214 4         22 JSON::Schema::Modern::Utilities::delete_media_type($media_type, refaddr $self);
1215             }
1216             }
1217              
1218             # deprecated interface; will use global definitions
1219 24     25 1 27442 sub get_media_type ($self, $type) {
  24         36  
  24         32  
  24         30  
1220 24 100       586 carp '$jsm->get_media_type is deprecated; use the function in JSON::Schema::Modern::Utilities instead' if not $warn_get_media_type++;
1221              
1222 24         608 JSON::Schema::Modern::Utilities::_get_media_type_decoder($type);
1223             }
1224              
1225             has _encoding => (
1226             is => 'bare',
1227             isa => HashRef[CodeRef],
1228             reader => '__encoding',
1229             lazy => 1,
1230             default => sub ($self) {
1231             +{
1232             identity => sub ($content_ref, @) { $content_ref },
1233             base64 => sub ($content_ref, @) {
1234             die "invalid characters\n"
1235             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
1236             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
1237             },
1238             base64url => sub ($content_ref, @) {
1239             die "invalid characters\n"
1240             if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
1241             require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
1242             },
1243             };
1244             },
1245             );
1246              
1247 23     24 1 560 sub get_encoding { $_[0]->__encoding->{$_[1]} }
1248 0     1 1 0 sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
1249              
1250             # callback hook for Sereal::Encoder
1251 3     4 0 696 sub FREEZE ($self, $serializer) {
  3         5  
  3         5  
  3         4  
1252 3         26 my $data = +{ %$self };
1253             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1254             # coderefs can't serialize cleanly and must be re-added by the user.
1255 3         15 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1256 3         75 return $data;
1257             }
1258              
1259             # callback hook for Sereal::Decoder
1260 4     5 0 190 sub THAW ($class, $serializer, $data) {
  4         8  
  4         6  
  4         6  
  4         6  
1261 4         7 my $self = bless($data, $class);
1262              
1263             # load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
1264             load_module($_)
1265 4         17 foreach uniqstr(
1266             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1267             (map $_->[1], values $self->__vocabulary_classes->%*));
1268              
1269 4         46 return $self;
1270             }
1271              
1272             1;
1273              
1274             __END__