File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 1014 1028 98.6
branch 265 322 82.3
condition 162 186 87.1
subroutine 229 229 100.0
pod 14 16 87.5
total 1684 1781 94.5


line stmt bran cond sub pod time code
1 50     50   13462097 use strict;
  50         84  
  50         1578  
2 50     50   195 use warnings;
  50         76  
  50         3532  
3             package JSON::Schema::Modern; # git description: v0.634-14-g40555ec8
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.635';
9              
10 50     50   699 use 5.020; # for fc, unicode_strings features
  50         140  
11 50     50   23496 use Moo;
  50         289960  
  50         205  
12 50     50   57626 use strictures 2;
  50         366  
  50         1574  
13 50     50   15953 use stable 0.031 'postderef';
  50         657  
  50         298  
14 50     50   7569 use experimental 'signatures';
  50         128  
  50         168  
15 50     50   2153 no autovivification warn => qw(fetch store exists delete);
  50         129  
  50         355  
16 50     49   3020 use if "$]" >= 5.022, experimental => 're_strict';
  49         97  
  49         1199  
17 49     49   3102 no if "$]" >= 5.031009, feature => 'indirect';
  49         92  
  49         2646  
18 49     49   229 no if "$]" >= 5.033001, feature => 'multidimensional';
  49         93  
  49         2070  
19 49     49   201 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  49         85  
  49         2032  
20 49     49   252 no if "$]" >= 5.041009, feature => 'smartmatch';
  49         85  
  49         1563  
21 49     49   324 no feature 'switch';
  49         158  
  49         1140  
22 49     49   20535 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  49         7291276  
  49         1720  
23 49     49   361 use Carp qw(croak carp);
  49         173  
  49         2726  
24 49     48   271 use List::Util 1.55 qw(pairs first uniqint pairmap uniq min);
  48         1099  
  47         3722  
25 47     48   280 use if "$]" < 5.041010, 'List::Util' => 'any';
  48         588  
  48         1873  
26 48     48   189 use if "$]" >= 5.041010, experimental => 'keyword_any';
  48         406  
  47         848  
27 47     48   21761 use builtin::compat qw(refaddr load_module);
  48         640084  
  48         2492  
28 48     48   24918 use Mojo::URL;
  48         371203  
  48         2766  
29 48     48   22066 use Safe::Isa;
  48         23059  
  48         8193  
30 48     48   18646 use Mojo::File 'path';
  48         677313  
  48         5438  
31 48     47   328 use Storable 'dclone';
  47         71  
  47         2785  
32 47     47   217 use File::ShareDir 'dist_dir';
  47         67  
  47         2354  
33 47     47   18799 use MooX::TypeTiny 0.002002;
  47         15195  
  47         222  
34 47     47   299540 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  47         4414665  
  47         559  
35 47     47   168465 use Digest::MD5 'md5';
  47         92  
  47         2819  
36 47     47   24119 use Feature::Compat::Try;
  47         14520  
  47         414  
37 47     47   21597 use JSON::Schema::Modern::Error;
  47         862  
  47         1715  
38 47     47   22257 use JSON::Schema::Modern::Result;
  47         459  
  47         1687  
39 47     47   22191 use JSON::Schema::Modern::Document;
  47         885  
  47         767  
40 47     47   2975 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         78  
  47         4990  
41 47     47   259 use namespace::clean;
  47         85  
  47         436  
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   25880 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  47         83  
  47         2976  
52 47     47   189 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  47         84  
  47         28683  
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   1120 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  969         1205  
  969         1322  
  969         1001  
  969         6908  
117              
118 12     12 1 24157 sub add_format_validation ($self, $format, $definition) {
  12         20  
  12         19  
  12         24  
  12         13  
119 12 50 100     63 return if exists(($self->{_format_validations}//{})->{$format});
120              
121 12 100       45 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
122 12         81 $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     236 if core_formats_type->check($format) and $definition->{type} ne 'string';
127              
128 47     47   393 use autovivification 'store';
  47         85  
  47         454  
129 4         2308 $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 83648 croak 'insufficient arguments' if @_ < 2;
151 17395         24382 my $self = shift;
152              
153 17395 100       63138 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
154 2         204 Carp::carp('use of deprecated form of add_schema with document');
155 2         8 return $self->add_document($_[0]);
156             }
157              
158             # TODO: resolve $uri against $self->base_uri
159 17394 50       161217 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
160             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
161              
162 17394 100       245720 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
163 17393 50       85575 croak 'insufficient arguments' if not @_;
164              
165 17393 100       33831 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
166 2         296 Carp::carp('use of deprecated form of add_schema with document');
167 2         11 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         475137 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         268404 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
181 17392 100       787683 if (my $existing_doc = first {
182 747228   66 747228   7499765 my $existing_checksum = $_->_checksum
183             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
184 747228 100       3194585 $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         2538195 $document = $existing_doc;
189             }
190              
191 17392         437812 $self->add_document($uri, $document);
192             }
193              
194             sub add_document {
195 18204 50   18204 1 93661 croak 'insufficient arguments' if @_ < 2;
196 18204         27751 my $self = shift;
197              
198             # TODO: resolve $uri against $self->base_uri
199 18204 50       54948 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       4708888 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
203 18204 50       86643 croak 'insufficient arguments' if not @_;
204              
205 18204         26488 my $document = shift;
206 18204 50       40560 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       201510 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       45282 if (not length $base_uri){
217 17191         1517805 foreach my $res_pair ($document->resource_pairs) {
218 18050         194399 my ($uri_string, $doc_resource) = @$res_pair;
219              
220             # this might croak if there are duplicates or malformed entries.
221 18050         118870 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
222             }
223              
224 17186         5437478 return $document;
225             }
226              
227 834         118493 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         2160 foreach my $res_pair ($document->resource_pairs) {
231 864         1697 my ($uri_string, $doc_resource) = @$res_pair;
232 864         2100 $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         367997 $doc_resource->%{qw(path specification_version vocabularies)},
237             document => $document,
238             };
239              
240 864   100     302591 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
241 47     47   49245 use autovivification 'store';
  47         91  
  47         190  
242             $new_resource->{anchors}{$anchor} = {
243             $doc_resource->{anchors}{$anchor}->%{path},
244             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
245 170 100       1546 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         56414 $self->_add_resource($uri_string => $new_resource);
251 857 100 66     294279 @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       3289 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
256              
257 827         126621 return $document;
258             }
259              
260 4     4 1 3092 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         10  
  4         17  
  4         11  
  4         9  
  4         136  
261 4 50       18 croak 'evaluate_json_string called in void context' if not defined wantarray;
262              
263 4         6 my $data;
264 4         14 try {
265 4         95 $data = $self->_json_decoder->decode($json_data)
266             }
267             catch ($e) {
268 3         93 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         18 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 57536 sub traverse ($self, $schema_reference, $config_override = {}) {
  17944         25260  
  17944         23459  
  17944         25015  
  17944         20463  
293 17944         46355 my %overrides = %$config_override;
294 17944         53567 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
295 17944 50       34379 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     72242 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
301 17944   100     5038098 my $initial_path = $config_override->{traversed_keyword_path} // '';
302 17944   100     106126 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
303              
304 17944 50       65814 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
305              
306 17944 100       40398 if (length(my $uri_path = $initial_uri->fragment)) {
307 5 50       24 croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
308              
309 5 50       21 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     263512 evaluator => $self,
326             traverse => 1,
327             };
328              
329 17944         32710 my $valid = 1;
330              
331 17944         30231 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     171493 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
339              
340 17944 100       59696 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
341 17938         379625 $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       1023 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         43 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
352             }
353              
354 17944 100 66     115736 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
355 17944 50 66     37592 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
356 17944 50 66     100770 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
357             }
358             catch ($e) {
359 1 0       23 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    0          
360 1         4 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         1 push $state->{errors}->@*, $e;
365             }
366             else {
367 1         8 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
368             }
369             }
370              
371 17944         68414 return $state;
372             }
373              
374             # the actual runtime evaluation of the schema against input data.
375 17391     17391 1 20944120 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17391         28770  
  17391         28818  
  17391         25313  
  17391         31577  
  17391         26980  
376 17391 50       44939 croak 'evaluate called in void context' if not defined wantarray;
377              
378 17391         38268 my %overrides = %$config_override;
379 17391         51094 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       37851 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     162301 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 17390 100       1379581 $state->{data} = jsonp_set('', $state->{data_path}, ref $data ? dclone($data) : $data);
393              
394 17390         26073 my $valid;
395 17390         30574 try {
396 17390 100 100     44750 if (is_schema($schema_reference)) {
    100          
397             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
398 17315         51105 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
399             }
400             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
401 4         35 abort($state, 'invalid schema type: %s', get_type($schema_reference));
402             }
403              
404 17201         140826 my $schema_info = $self->_fetch_from_uri($schema_reference);
405 17201 100       36664 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       68576 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     1900867 my $val = $config_override->{$_} // $self->$_;
422 120358 100       688883 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     110248 $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       64208 $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     56859 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
436              
437 17194         62104 $valid = $self->_evaluate_subschema($data, $schema_info->{schema}, $state);
438 17171 50 66     55543 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
439 17171 50 66     100749 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
440             }
441             catch ($e) {
442 219 100       997 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
443 180         2953 return $e;
444             }
445             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
446 34         711 push $state->{errors}->@*, $e;
447             }
448             else {
449 5         113 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
450             }
451             }
452              
453 17210 100       42407 if ($state->{seen_data_properties}) {
454 5         13 my %unknown_keywords;
455 5         54 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
456             keys $state->{seen_data_properties}->%*) {
457 15         55 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
458 15   100     53 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
459             }
460              
461 5         18 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       112 join(', ', sort $unknown_keywords{$parent}->@*));
465             }
466             }
467              
468 17210 50 50     63802 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     475175 );
    100          
    100          
481             }
482              
483 10     11 1 19539 sub validate_schema ($self, $schema, $config_override = {}) {
  10         20  
  10         14  
  10         19  
  10         15  
484 10 50       101 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     113 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
488              
489             my $result = $self->evaluate($schema, $metaschema_uri,
490 10 100 100     90 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
491              
492 10 100       188 return $result if not $result->valid;
493              
494             # the traversal pass will validate all constraints that weren't handled by the metaschema
495 3         58 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       33 ) if $state->{errors}->@*;
501              
502 2         28 return $result; # valid: true
503             }
504              
505 8     9 1 30196 sub get ($self, $uri_reference) {
  8         14  
  8         15  
  8         13  
506 8 100       23 if (wantarray) {
507 5         20 my $schema_info = $self->_fetch_from_uri($uri_reference);
508 5 100       24 return if not $schema_info;
509 4 100       406 my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
510 4         34 return ($subschema, $schema_info->{canonical_uri});
511             }
512             else { # abridged version of _fetch_from_uri
513 3 50       22 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
514 3         410 my $fragment = $uri_reference->fragment;
515 3         20 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
516 3 50       114 return if not $resource;
517              
518 3         6 my $schema;
519 3 100 100     20 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       228 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   56597 sub _traverse_subschema ($self, $schema, $state) {
  42172         51355  
  42172         47322  
  42172         44685  
  42172         44668  
583 42172         289252 delete $state->@{'keyword', grep /^_/, keys %$state};
584              
585             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
586 42172 50       157861 if $state->{depth}++ > $self->max_traversal_depth;
587              
588 42172         157271 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
589              
590 42172         102064 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     126277 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
594              
595 34462 100       60165 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
596              
597 34445 100       81744 return 1 if not keys %$schema;
598              
599 33246         43720 my $valid = 1;
600 33246         166282 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         97723 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
608 203351         302484 my $vocabulary = $state->{vocabularies}[$vocab_index];
609 203351         197814 my $keyword_list;
610              
611 203351   66     296445 for (my $keyword_index = 0;
612             $keyword_index < ($keyword_list //= do {
613 47     47   145883 use autovivification qw(fetch store);
  47         82  
  47         207  
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     608339 ];
618             })->@*;
619             $keyword_index++) {
620 1684836         2210697 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
621 1684836 100       3522389 next if not exists $schema->{$keyword};
622              
623             # keywords adjacent to $ref are not evaluated before draft2019-09
624 56872 100 100     194741 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
625              
626 56836         95106 delete $unknown_keywords{$keyword};
627 56836         95168 $state->{keyword} = $keyword;
628              
629 56836         84015 my $old_spec_version = $state->{specification_version};
630 56836         89481 my $error_count = $state->{errors}->@*;
631              
632 56836 100       161748 if (not $sub->($vocabulary, $schema, $state)) {
633             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
634 243 50       796 if $error_count == $state->{errors}->@*;
635 243         351 $valid = 0;
636 243         870 next;
637             }
638             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
639 56593 50       167516 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       114535 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
643              
644 56593 100       216253 if (my $callback = $state->{callbacks}{$keyword}) {
645 4         9 $error_count = $state->{errors}->@*;
646              
647 4 50       10 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       1985 if $error_count != $state->{errors}->@*;
655             }
656             }
657             }
658              
659 33246         64149 delete $state->{keyword};
660              
661 33246 100 100     88834 if ($self->strict and keys %unknown_keywords) {
662 2 50       14 $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         157069 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
668 104050 100       172429 next if not exists $schema->{$keyword};
669 224         742 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
670             .canonical_uri($state).'")';
671 224 50       24226 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
672 224         994 my @list = map '"'.$_.'"', @$alternates;
673 224 50       560 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
674 224 100       676 splice(@list, -1, 0, 'or') if @list > 1;
675 224         597 $message .= ': this should be rewritten as '.join(' ', @list);
676             }
677 224         50153 carp $message;
678             }
679              
680 33246         141998 return $valid;
681             }
682              
683 34834     34835   44089 sub _evaluate_subschema ($self, $data, $schema, $state) {
  34834         40539  
  34834         44457  
  34834         41171  
  34834         40521  
  34834         40768  
684 34834 50       62239 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     119956 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
689 34834         278796 delete $state->@{'keyword', grep /^_/, keys %$state};
690              
691             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
692 34834 100       128714 if $state->{depth}++ > $self->max_traversal_depth;
693              
694 34831         95575 my $schema_type = get_type($schema);
695 34831 100 66     71684 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       59259 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
699              
700 33987 100       74035 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         80219 my $canonical_uri = canonical_uri($state);
705 33349         81571 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
706             {
707 47     47   49075 use autovivification qw(fetch store);
  47         89  
  47         223  
  33349         38548  
708             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
709             if grep substr($schema_location, 0, length) eq $_,
710 33349 100       121425 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
711 33348         4011948 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
712             }
713              
714 33348         3178978 my $valid = 1;
715 33348         215583 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         72448 my $parent_annotations = $state->{annotations};
719 33348         65659 $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     246844 && (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         63859 my $defaults = $state->{defaults};
732 33348 100       68368 $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         106244 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
741 183051         273332 my $vocabulary = $state->{vocabularies}[$vocab_index];
742 183051         177284 my $keyword_list;
743              
744 183051   66     270540 for (my $keyword_index = 0;
745             $keyword_index < ($keyword_list //= do {
746 47     47   14852 use autovivification qw(fetch store);
  47         124  
  47         345  
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     587940 ];
751             })->@*;
752             $keyword_index++) {
753 1510156         1978269 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
754 1510156 100       3132583 next if not exists $schema->{$keyword};
755              
756             # keywords adjacent to $ref are not evaluated before draft2019-09
757 60037 100 100     184429 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
758              
759 60006         97521 delete $unknown_keywords{$keyword};
760 60006 100 100     116088 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
761              
762 60005         114192 $state->{keyword} = $keyword;
763              
764 60005 100       92021 if ($sub) {
765 55929         83538 my $old_spec_version = $state->{specification_version};
766 55929         93613 my $error_count = $state->{errors}->@*;
767              
768 55929         80151 try {
769 55929 100       163740 if (not $sub->($vocabulary, $data, $schema, $state)) {
770             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
771 13161 50       45000 if $error_count == $state->{errors}->@*;
772 13161         18129 $valid = 0;
773              
774 13161 100 100     59489 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
775 6948         29446 next;
776             }
777              
778             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
779 42724 50       271099 if $error_count != $state->{errors}->@*;
780             }
781             catch ($e) {
782 44 100       789 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       98717 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
788             }
789              
790 46800 100 100     180660 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
791 24         42 my $error_count = $state->{errors}->@*;
792              
793 24 100       61 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         3 $valid = 0;
797              
798 2 100 66     13 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       550 if $error_count != $state->{errors}->@*;
803             }
804             }
805             }
806              
807 33304         68806 delete $state->{keyword};
808              
809 33304 100 100     66722 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     71477 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         1006 foreach grep !/^x-/, keys %$data;
820              
821             my @evaluated_properties = map {
822 156         333 my $keyword = $_->{keyword};
  577         733  
823             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
824 577 100       1252 ? $_->{annotation}->@* : ();
825             } local_annotations($state);
826              
827             # tick off properties that were recognized by this subschema
828 156         311 $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     388 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
833             }
834              
835 33301 100 100     96551 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         7512 foreach sort keys %unknown_keywords;
838             }
839              
840             # only keep new annotations if schema is valid
841 33301 100       73945 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     76773 if $valid and $state->{defaults};
846              
847 33301         227634 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 44035 50   44036   101693 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
870 44035   100     2656229 ($_[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   65124 use autovivification 'store';
  47         89  
  47         251  
876             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
877 103     104   971 foreach pairs @_[1..$#_];
878             }
879 25   50 26   11584 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
880 17395   100 17396   2648166 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
881 2123   50 2124   248451 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
882              
883 18976     18977   44832 sub _add_resource ($self, @kvs) {
  18976         24887  
  18976         31180  
  18976         22965  
884 18976         73717 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
885 18976         33036 my ($canonical_uri, $resource) = @$pair;
886              
887 18976 100       43897 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 17055 100       47445 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       5579 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
896 859 100       239262 next if not @diffs;
897 10         2768 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         539 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
903             }
904              
905 47     47   22865 use autovivification 'store';
  47         92  
  47         183  
906 18115         85592 $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   798 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   28  
  12     13   231  
  12     12   443  
  12     12   21  
  12     12   153  
  12     12   529  
  12     12   22  
  12     7   186  
  12     7   610  
  12     7   22  
  12     7   145  
  12     7   494  
  12     7   22  
  12     7   183  
  12     7   454  
  12     1   18  
  12     1   130  
  12     1   36  
  12     1   14  
  12     1   144  
  12     1   32  
  12         20  
  12         34  
  7         40  
  7         8  
  7         150  
  7         37  
  7         10  
  7         87  
  7         23  
  7         10  
  7         87  
  7         19  
  7         11  
  7         69  
  7         19  
  7         9  
  7         94  
  7         25  
  7         9  
  7         55  
  7         20  
  7         10  
  7         75  
  7         22  
  7         9  
  7         28  
  1         3  
  1         2  
  1         9  
  1         2  
  1         2  
  1         9  
  1         3  
  1         1  
  1         8  
  1         3  
  1         2  
  1         7  
  1         2  
  1         2  
  1         8  
  1         3  
  1         1  
  1         4  
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   1886 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
931              
932 10     11 1 19540 sub add_vocabulary ($self, $classname) {
  10         18  
  10         13  
  10         35  
933 10 50       239 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
934              
935 10         825 $vocabulary_class_type->(load_module($classname));
936              
937             # uri => version, uri => version
938 7         517 foreach my $pair (pairs $classname->vocabulary) {
939 7         125 my ($uri_string, $spec_version) = @$pair;
940 7         45 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
941 6         7137 $spec_version_type->($spec_version);
942              
943 4 100       489 croak 'keywords starting with "$" are reserved for core and cannot be used'
944             if grep /^\$/, $classname->keywords;
945              
946 3         25 $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   20389 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   123  
  38     38   1116  
  38     38   17368  
  38     38   172  
  38     38   1268  
  38     38   19591  
  38     30   159  
  38     30   1204  
  38     30   18439  
  38     30   155  
  38     30   1033  
  38     30   17593  
  38     30   179  
  38     19   1076  
  38     19   15957  
  38     19   150  
  38     19   1198  
  38     19   225  
  38     19   58  
  38     19   294  
  30     17   185  
  30     16   62  
  30     16   488  
  30     16   103  
  30     16   52  
  30     16   395  
  30     16   134  
  30     13   77  
  30     13   335  
  30     13   3269  
  30     13   51  
  30     13   368  
  30     13   95  
  30     13   3490  
  30     12   356  
  30     12   94  
  30     12   46  
  30     12   320  
  30     12   104  
  30     12   56  
  30     12   184  
  19     11   193  
  19     11   36  
  19     11   273  
  19     11   62  
  19     11   42  
  19     11   256  
  19     11   68  
  19     10   30  
  19     10   201  
  19     10   55  
  19     9   31  
  19     9   195  
  19     9   60  
  19     9   29  
  19     9   273  
  19     9   86  
  19     9   29  
  19     9   211  
  19     9   61  
  19     9   38  
  19     9   81  
  17     9   101  
  17     9   30  
  17     9   284  
  16     9   58  
  16     9   25  
  16     9   181  
  16     9   62  
  16     8   24  
  16     8   176  
  16     8   51  
  16     8   38  
  16     8   286  
  16     8   56  
  16     8   23  
  16     6   201  
  16     6   54  
  16     6   23  
  16     6   234  
  16     6   50  
  16     6   28  
  16     6   83  
  13     6   75  
  13     6   25  
  13     6   225  
  13     6   46  
  13     6   21  
  13     6   126  
  13     6   45  
  13     6   21  
  13     6   189  
  13     6   40  
  13     5   16  
  13     5   174  
  13     5   42  
  13     5   16  
  13     5   156  
  13     5   37  
  13     5   19  
  13     5   3345  
  13     5   43  
  13     5   17  
  13     5   57  
  12     6   69  
  12     6   23  
  12     6   197  
  12     6   58  
  12     6   25  
  12     6   158  
  12     6   49  
  12     6   47  
  12     4   131  
  12     4   36  
  12     4   18  
  12     4   131  
  12     4   35  
  12     4   20  
  12     4   175  
  12     4   67  
  12     4   17  
  12     4   196  
  12     4   40  
  12     4   18  
  12     4   90  
  11         62  
  11         17  
  11         201  
  11         42  
  11         16  
  11         136  
  11         41  
  11         18  
  11         125  
  11         30  
  11         20  
  11         176  
  11         38  
  11         17  
  11         126  
  11         47  
  11         36  
  11         192  
  11         37  
  11         16  
  11         49  
  10         70  
  10         20  
  10         171  
  10         35  
  10         18  
  10         110  
  9         30  
  9         16  
  9         144  
  9         27  
  9         16  
  9         129  
  9         27  
  9         12  
  9         83  
  9         27  
  9         15  
  9         104  
  9         30  
  9         11  
  9         36  
  9         54  
  9         15  
  9         136  
  9         33  
  9         31  
  9         111  
  9         32  
  9         17  
  9         98  
  9         24  
  9         14  
  9         90  
  9         27  
  9         12  
  9         122  
  9         26  
  9         13  
  9         87  
  9         24  
  9         14  
  9         52  
  9         54  
  9         17  
  9         177  
  9         47  
  9         14  
  9         109  
  9         33  
  9         15  
  9         94  
  9         25  
  9         17  
  9         86  
  9         24  
  9         13  
  9         106  
  9         29  
  9         13  
  9         121  
  9         38  
  9         15  
  9         54  
  8         45  
  8         13  
  8         103  
  8         27  
  8         13  
  8         81  
  8         26  
  8         12  
  8         87  
  8         21  
  8         13  
  8         93  
  8         26  
  8         10  
  8         71  
  8         26  
  8         11  
  8         97  
  8         24  
  8         13  
  8         33  
  6         45  
  6         12  
  6         95  
  6         20  
  6         8  
  6         52  
  6         36  
  6         12  
  6         96  
  6         19  
  6         7  
  6         61  
  6         23  
  6         9  
  6         55  
  6         16  
  6         11  
  6         86  
  6         19  
  6         10  
  6         31  
  6         35  
  6         13  
  6         87  
  6         21  
  6         11  
  6         65  
  6         21  
  6         9  
  6         55  
  6         18  
  6         9  
  6         70  
  6         20  
  6         13  
  6         62  
  6         15  
  6         8  
  6         70  
  6         18  
  6         10  
  6         22  
  6         470  
  6         4491  
  6         104  
  6         490  
  6         3835  
  6         79  
  6         370  
  6         3184  
  6         65  
  5         16  
  5         9  
  5         78  
  5         17  
  5         9  
  5         66  
  5         16  
  5         8  
  5         55  
  5         17  
  5         6  
  5         22  
  5         27  
  5         9  
  5         71  
  5         17  
  5         8  
  5         41  
  5         19  
  5         19  
  5         50  
  5         16  
  5         7  
  5         56  
  5         14  
  5         9  
  5         37  
  5         13  
  5         8  
  5         48  
  5         15  
  5         8  
  5         22  
  6         31  
  6         11  
  6         82  
  6         19  
  6         8  
  6         52  
  6         35  
  6         9  
  6         72  
  6         16  
  6         11  
  6         70  
  6         16  
  6         10  
  6         66  
  6         18  
  6         9  
  6         58  
  6         17  
  6         8  
  6         58  
  6         26  
  6         12  
  6         57  
  4         17  
  4         5  
  4         32  
  4         16  
  4         6  
  4         39  
  4         11  
  4         6  
  4         34  
  4         13  
  4         7  
  4         44  
  4         12  
  4         7  
  4         31  
  4         11  
  4         6  
  4         19  
  4         21  
  4         9  
  4         74  
  4         15  
  4         7  
  4         31  
  4         17  
  4         8  
  4         73  
  4         14  
  4         6  
  4         80  
  4         18  
  4         8  
  4         60  
  4         15  
  4         6  
  4         41  
  4         14  
  4         304  
  4         66  
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   610946 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
977 5924     5925   28929 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
978 4     5   181 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   82 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         70  
  53         83  
  53         103  
  53         66  
982 53 100       231 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         10 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
986 2         4 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
987             }
988              
989 51         75 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       168 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
993 51 100       169 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
994              
995 51 100       143 return (undef, []) if not $valid;
996              
997 49         74 my @vocabulary_classes;
998              
999 49         295 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
1000 105         249 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     9473 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1003              
1004 97 100       829 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1005              
1006 89         175 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       294 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       197 $a->evaluation_order <=> $b->evaluation_order
  49 50       174  
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     209 $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         93 my %seen_keyword;
1024 49         112 foreach my $class (@vocabulary_classes) {
1025 83         292 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       1014 if $seen_keyword{$keyword};
1029 794         1317 $seen_keyword{$keyword} = $class;
1030             }
1031             }
1032              
1033 49 100       574 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         5676 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   65928 };
  47         88  
1045              
1046             # for internal use only. files are under share/
1047 47         49205 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   265 };
  47         213  
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   885909 sub _get_or_load_resource ($self, $uri) {
  24428         31518  
  24428         30008  
  24428         26265  
1085 24428         56614 my $resource = $self->_get_resource($uri);
1086 24428 100       2041475 return $resource if $resource;
1087              
1088 114 100       457 if (my $document = load_cached_document($self, $uri)) {
1089 103         348 return $self->_get_resource($uri);
1090             }
1091              
1092             # TODO:
1093             # - load from network or disk
1094              
1095 11         28 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   223497 sub _fetch_from_uri ($self, $uri_reference) {
  24190         33967  
  24190         30008  
  24190         29862  
1107 24190 50       72671 $uri_reference = Mojo::URL->new($uri_reference) if not is_schema($uri_reference);
1108              
1109             # this is *a* resource that would contain our desired location, but may not be the closest one
1110 24190         5754029 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1111 24190 100       109734 return if not $resource;
1112              
1113 24179         44177 my $fragment = $uri_reference->fragment;
1114 24179 100 100     112381 if (not length($fragment) or $fragment =~ m{^/}) {
1115 23599   100     175063 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1116 23599 100       171144 return if not defined $subschema;
1117              
1118 23597         29409 my $closest_resource;
1119 23597 100       41008 if (not length $fragment) { # we already have the canonical resource root
1120 21474         37744 $closest_resource = [ undef, $resource ];
1121             }
1122             else {
1123             # determine the canonical uri by finding the closest schema resource(s)
1124 2123         5473 my $doc_addr = refaddr($resource->{document});
1125             my @closest_resources =
1126 506         2799 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1127             grep { !length($_->[1]{path}) # document root
1128 2913 100 66     26370 || length($document_path)
1129             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1130 2123         6042 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228352         349601  
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       37243 if (@closest_resources > 1) {
1135             # find the resource key that most closely matches the original query uri, by matching prefixes
1136 464         2151 my $match = $uri_reference.'';
1137             @closest_resources =
1138 26         103 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         84532 @closest_resources;
1141             }
1142              
1143 2123         4501 $closest_resource = $closest_resources[0];
1144             }
1145              
1146             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1147 23597         93776 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1148 23597 100       1506008 $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         344244 $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     3133 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         3194 $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   67 sub _prefix_match_length ($x, $y) {
  52         71  
  52         60  
  52         58  
1171 52         123 my $len = min(length($x), length($y));
1172 52         119 foreach my $pos (0..$len) {
1173 1456 100       2113 return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
1174             }
1175 0         0 return $len;
1176             }
1177              
1178             # Mojo::JSON::JSON_XS is false when the environment variable $MOJO_NO_JSON_XS is set
1179             # and also checks if Cpanel::JSON::XS is installed.
1180             # Mojo::JSON falls back to its own pure-perl encoder/decoder but does not support all the options
1181             # that we require here.
1182             use constant _JSON_BACKEND =>
1183 47         760 Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION('4.38'); 1 } ? 'Cpanel::JSON::XS'
  47         60513  
1184 47 0       84 : eval { JSON::PP->VERSION('4.11'); 1 } ? 'JSON::PP'
  2 50       2  
  2         17  
1185 47     47   336 : die 'Cpanel::JSON::XS 4.38 or JSON::PP 4.11 is required';
  47         74  
1186              
1187             # used for internal encoding as well (when caching serialized schemas)
1188             has _json_decoder => (
1189             is => 'ro',
1190             isa => HasMethods[qw(encode decode)],
1191             lazy => 1,
1192             default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
1193             );
1194              
1195             # since media types are case-insensitive, all type names must be casefolded on insertion.
1196             has _media_type => (
1197             is => 'bare',
1198             isa => my $media_type_type = Map[Str->where(q{$_ eq CORE::fc($_)}), CodeRef],
1199             reader => '__media_type',
1200             lazy => 1,
1201             default => sub ($self) {
1202             my $_json_media_type = sub ($content_ref) {
1203             # utf-8 decoding is always done, as per the JSON spec.
1204             # other charsets are not supported: see RFC8259 ยง11
1205             \ _JSON_BACKEND->new->allow_nonref(1)->utf8(1)->decode($content_ref->$*);
1206             };
1207             +{
1208             (map +($_ => $_json_media_type),
1209             qw(application/json application/schema+json application/schema-instance+json)),
1210             (map +($_ => sub ($content_ref) { $content_ref }),
1211             qw(text/* application/octet-stream)),
1212             'application/x-www-form-urlencoded' => sub ($content_ref) {
1213             \ Mojo::Parameters->new->charset('UTF-8')->parse($content_ref->$*)->to_hash;
1214             },
1215             'application/x-ndjson' => sub ($content_ref) {
1216             my $decoder = _JSON_BACKEND->new->allow_nonref(1)->utf8(1);
1217             my $line = 0; # line numbers start at 1
1218             \[ map {
1219             do {
1220             try { ++$line; $decoder->decode($_) }
1221             catch ($e) { die 'parse error at line '.$line.': '.$e }
1222             }
1223             }
1224             split(/\r?\n/, $content_ref->$*)
1225             ];
1226             },
1227             };
1228             },
1229             );
1230              
1231 5     6 1 2619 sub add_media_type { $media_type_type->({ @_[1..2] }); $_[0]->__media_type->{$_[1]} = $_[2]; }
  4         267  
1232              
1233             # get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
1234 39     40 1 9846 sub get_media_type ($self, $type) {
  39         53  
  39         60  
  39         48  
1235 39         826 my $types = $self->__media_type;
1236 39         765 my $mt = $types->{fc $type};
1237 39 100       131 return $mt if $mt;
1238              
1239 9 100 100 60   65 return $types->{(first { m{([^/]+)/\*\z} && fc($type) =~ m{^\Q$1\E/[^/]+\z} } keys %$types) // '*/*'};
  59         347  
1240             };
1241              
1242             has _encoding => (
1243             is => 'bare',
1244             isa => HashRef[CodeRef],
1245             reader => '__encoding',
1246             lazy => 1,
1247             default => sub ($self) {
1248             +{
1249             identity => sub ($content_ref) { $content_ref },
1250             base64 => sub ($content_ref) {
1251             die "invalid characters\n"
1252             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
1253             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
1254             },
1255             base64url => sub ($content_ref) {
1256             die "invalid characters\n"
1257             if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
1258             require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
1259             },
1260             };
1261             },
1262             );
1263              
1264 23     24 1 568 sub get_encoding { $_[0]->__encoding->{$_[1]} }
1265 0     1 1 0 sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
1266              
1267             # callback hook for Sereal::Encoder
1268 3     4 0 674 sub FREEZE ($self, $serializer) {
  3         6  
  3         6  
  3         3  
1269 3         49 my $data = +{ %$self };
1270             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1271             # coderefs can't serialize cleanly and must be re-added by the user.
1272 3         15 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1273 3         77 return $data;
1274             }
1275              
1276             # callback hook for Sereal::Decoder
1277 4     5 0 192 sub THAW ($class, $serializer, $data) {
  4         7  
  4         98  
  4         7  
  4         8  
1278 4         7 my $self = bless($data, $class);
1279              
1280             # load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
1281             load_module($_)
1282 4         19 foreach uniq(
1283             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1284             (map $_->[1], values $self->__vocabulary_classes->%*));
1285              
1286 4         104 return $self;
1287             }
1288              
1289             1;
1290              
1291             __END__