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   14131249 use strict;
  50         93  
  50         1705  
2 50     50   216 use warnings;
  50         91  
  50         4168  
3             package JSON::Schema::Modern; # git description: v0.636-3-ga9ec6cf8
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.637';
9              
10 50     50   823 use 5.020; # for fc, unicode_strings features
  50         151  
11 50     50   25522 use Moo;
  50         308739  
  50         208  
12 50     50   61574 use strictures 2;
  50         429  
  50         1804  
13 50     50   17327 use stable 0.031 'postderef';
  50         720  
  50         339  
14 50     50   8158 use experimental 'signatures';
  50         152  
  50         205  
15 50     50   2582 no autovivification warn => qw(fetch store exists delete);
  50         112  
  50         344  
16 50     49   3296 use if "$]" >= 5.022, experimental => 're_strict';
  49         105  
  49         1179  
17 49     49   3384 no if "$]" >= 5.031009, feature => 'indirect';
  49         88  
  49         2623  
18 49     49   245 no if "$]" >= 5.033001, feature => 'multidimensional';
  49         81  
  49         2140  
19 49     49   251 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  49         111  
  49         2160  
20 49     49   258 no if "$]" >= 5.041009, feature => 'smartmatch';
  49         108  
  49         1628  
21 49     49   343 no feature 'switch';
  49         191  
  49         1329  
22 49     49   22858 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  49         7601246  
  49         1938  
23 49     49   380 use Carp qw(croak carp);
  49         232  
  49         3273  
24 49     48   263 use List::Util 1.55 qw(pairs first uniqint pairmap uniq min);
  48         1282  
  47         4064  
25 47     48   279 use if "$]" < 5.041010, 'List::Util' => 'any';
  48         560  
  48         1933  
26 48     48   189 use if "$]" >= 5.041010, experimental => 'keyword_any';
  48         447  
  47         936  
27 47     48   24072 use builtin::compat qw(refaddr load_module);
  48         686451  
  48         2668  
28 48     48   27910 use Mojo::URL;
  48         377339  
  48         3162  
29 48     48   23313 use Safe::Isa;
  48         24860  
  48         9005  
30 48     48   20016 use Mojo::File 'path';
  48         702440  
  48         5919  
31 48     47   372 use Storable 'dclone';
  47         81  
  47         3051  
32 47     47   244 use File::ShareDir 'dist_dir';
  47         78  
  47         2689  
33 47     47   20466 use MooX::TypeTiny 0.002002;
  47         15871  
  47         245  
34 47     47   316299 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  47         4715155  
  47         659  
35 47     47   185430 use Digest::MD5 'md5';
  47         102  
  47         3064  
36 47     47   25612 use Feature::Compat::Try;
  47         16415  
  47         283  
37 47     47   24213 use JSON::Schema::Modern::Error;
  47         906  
  47         1713  
38 47     47   23209 use JSON::Schema::Modern::Result;
  47         507  
  47         1921  
39 47     47   23672 use JSON::Schema::Modern::Document;
  47         844  
  47         780  
40 47     47   2900 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         81  
  47         5103  
41 47     47   268 use namespace::clean;
  47         74  
  47         421  
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   26866 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  47         89  
  47         3363  
52 47     47   206 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  47         94  
  47         29339  
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   1377 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  969         1037  
  969         1155  
  969         1034  
  969         6743  
117              
118 12     12 1 23405 sub add_format_validation ($self, $format, $definition) {
  12         25  
  12         18  
  12         25  
  12         17  
119 12 50 100     72 return if exists(($self->{_format_validations}//{})->{$format});
120              
121 12 100       48 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
122 12         83 $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   360 use autovivification 'store';
  47         91  
  47         402  
129 4         2266 $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 91620 croak 'insufficient arguments' if @_ < 2;
151 17395         26068 my $self = shift;
152              
153 17395 100       64314 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
154 2         172 Carp::carp('use of deprecated form of add_schema with document');
155 2         9 return $self->add_document($_[0]);
156             }
157              
158             # TODO: resolve $uri against $self->base_uri
159 17394 50       171130 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
160             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
161              
162 17394 100       251491 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
163 17393 50       90515 croak 'insufficient arguments' if not @_;
164              
165 17393 100       38201 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
166 2         251 Carp::carp('use of deprecated form of add_schema with document');
167 2         9 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         505461 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         291210 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
181 17392 100       894644 if (my $existing_doc = first {
182 684671   66 684671   7270140 my $existing_checksum = $_->_checksum
183             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
184 684671 100       3184404 $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         2693756 $document = $existing_doc;
189             }
190              
191 17392         468228 $self->add_document($uri, $document);
192             }
193              
194             sub add_document {
195 18204 50   18204 1 97507 croak 'insufficient arguments' if @_ < 2;
196 18204         29129 my $self = shift;
197              
198             # TODO: resolve $uri against $self->base_uri
199 18204 50       64423 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       4956814 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
203 18204 50       93744 croak 'insufficient arguments' if not @_;
204              
205 18204         28838 my $document = shift;
206 18204 50       44488 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       218162 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       50407 if (not length $base_uri){
217 17191         1610385 foreach my $res_pair ($document->resource_pairs) {
218 18050         207577 my ($uri_string, $doc_resource) = @$res_pair;
219              
220             # this might croak if there are duplicates or malformed entries.
221 18050         127047 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
222             }
223              
224 17186         5848026 return $document;
225             }
226              
227 834         125274 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         2662 foreach my $res_pair ($document->resource_pairs) {
231 864         1831 my ($uri_string, $doc_resource) = @$res_pair;
232 864         2257 $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         385067 $doc_resource->%{qw(path specification_version vocabularies)},
237             document => $document,
238             };
239              
240 864   100     317408 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
241 47     47   50154 use autovivification 'store';
  47         85  
  47         218  
242             $new_resource->{anchors}{$anchor} = {
243             $doc_resource->{anchors}{$anchor}->%{path},
244             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
245 170 100       1927 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         61891 $self->_add_resource($uri_string => $new_resource);
251 857 100 66     302975 @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       3342 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
256              
257 827         140449 return $document;
258             }
259              
260 4     4 1 3848 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         131  
  4         19  
  4         11  
  4         7  
  4         14  
261 4 50       17 croak 'evaluate_json_string called in void context' if not defined wantarray;
262              
263 4         8 my $data;
264 4         14 try {
265 4         104 $data = $self->_json_decoder->decode($json_data)
266             }
267             catch ($e) {
268 3         107 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         22 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 60532 sub traverse ($self, $schema_reference, $config_override = {}) {
  17944         24336  
  17944         26982  
  17944         24963  
  17944         22193  
293 17944         54658 my %overrides = %$config_override;
294 17944         61916 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
295 17944 50       40563 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     82130 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
301 17944   100     5243152 my $initial_path = $config_override->{traversed_keyword_path} // '';
302 17944   100     119050 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
303              
304 17944 50       70698 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
305              
306 17944 100       41209 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       23 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     281902 evaluator => $self,
326             traverse => 1,
327             };
328              
329 17944         35462 my $valid = 1;
330              
331 17944         30409 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     195332 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
339              
340 17944 100       64647 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
341 17938         403228 $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       1041 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         28 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
352             }
353              
354 17944 100 66     124777 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
355 17944 50 66     42453 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
356 17944 50 66     109535 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
357             }
358             catch ($e) {
359 1 0       8 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         15 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
368             }
369             }
370              
371 17944         73083 return $state;
372             }
373              
374             # the actual runtime evaluation of the schema against input data.
375 17391     17391 1 22213183 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17391         41015  
  17391         33604  
  17391         32746  
  17391         33249  
  17391         30046  
376 17391 50       49973 croak 'evaluate called in void context' if not defined wantarray;
377              
378 17391         40372 my %overrides = %$config_override;
379 17391         57138 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       42022 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     170854 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       1218765 $state->{data} = jsonp_set('', $state->{data_path}, ref $data ? dclone($data) : $data);
393              
394 17390         27844 my $valid;
395 17390         30090 try {
396 17390 100 100     46489 if (is_schema($schema_reference)) {
    100          
397             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
398 17315         56952 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
399             }
400             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
401 4         52 abort($state, 'invalid schema type: %s', get_type($schema_reference));
402             }
403              
404 17201         159378 my $schema_info = $self->_fetch_from_uri($schema_reference);
405 17201 100       43400 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       81731 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     2053596 my $val = $config_override->{$_} // $self->$_;
422 120358 100       773522 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     124267 $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       69812 $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     64142 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
436              
437 17194         70881 $valid = $self->_evaluate_subschema($data, $schema_info->{schema}, $state);
438 17171 50 66     60581 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
439 17171 50 66     108002 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
440             }
441             catch ($e) {
442 219 100       966 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
443 180         2819 return $e;
444             }
445             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
446 34         714 push $state->{errors}->@*, $e;
447             }
448             else {
449 5         141 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
450             }
451             }
452              
453 17210 100       49264 if ($state->{seen_data_properties}) {
454 5         13 my %unknown_keywords;
455 5         65 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
456             keys $state->{seen_data_properties}->%*) {
457 15         59 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
458 15   100     55 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
459             }
460              
461 5         22 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       119 join(', ', sort $unknown_keywords{$parent}->@*));
465             }
466             }
467              
468 17210 50 50     77314 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     529769 );
    100          
    100          
481             }
482              
483 10     11 1 27240 sub validate_schema ($self, $schema, $config_override = {}) {
  10         27  
  10         22  
  10         21  
  10         19  
484 10 50       194 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     148 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
488              
489             my $result = $self->evaluate($schema, $metaschema_uri,
490 10 100 100     119 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
491              
492 10 100       264 return $result if not $result->valid;
493              
494             # the traversal pass will validate all constraints that weren't handled by the metaschema
495 3         95 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       45 ) if $state->{errors}->@*;
501              
502 2         44 return $result; # valid: true
503             }
504              
505 8     9 1 29054 sub get ($self, $uri_reference) {
  8         15  
  8         12  
  8         14  
506 8 100       37 if (wantarray) {
507 5         18 my $schema_info = $self->_fetch_from_uri($uri_reference);
508 5 100       21 return if not $schema_info;
509 4 100       366 my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
510 4         31 return ($subschema, $schema_info->{canonical_uri});
511             }
512             else { # abridged version of _fetch_from_uri
513 3 50       23 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
514 3         356 my $fragment = $uri_reference->fragment;
515 3         13 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
516 3 50       141 return if not $resource;
517              
518 3         4 my $schema;
519 3 100 100     20 if (not length($fragment) or $fragment =~ m{^/}) {
520 2   100     20 $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       193 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   61042 sub _traverse_subschema ($self, $schema, $state) {
  42172         53762  
  42172         50662  
  42172         46659  
  42172         47750  
583 42172         306447 delete $state->@{'keyword', grep /^_/, keys %$state};
584              
585             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
586 42172 50       167532 if $state->{depth}++ > $self->max_traversal_depth;
587              
588 42172         167799 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
589              
590 42172         112764 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     142116 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
594              
595 34462 100       65269 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
596              
597 34445 100       92888 return 1 if not keys %$schema;
598              
599 33246         47015 my $valid = 1;
600 33246         178216 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         107243 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
608 203351         327855 my $vocabulary = $state->{vocabularies}[$vocab_index];
609 203351         211709 my $keyword_list;
610              
611 203351   66     308436 for (my $keyword_index = 0;
612             $keyword_index < ($keyword_list //= do {
613 47     47   149228 use autovivification qw(fetch store);
  47         100  
  47         220  
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     643638 ];
618             })->@*;
619             $keyword_index++) {
620 1684836         2329558 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
621 1684836 100       3677602 next if not exists $schema->{$keyword};
622              
623             # keywords adjacent to $ref are not evaluated before draft2019-09
624 56872 100 100     210087 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
625              
626 56836         97548 delete $unknown_keywords{$keyword};
627 56836         110285 $state->{keyword} = $keyword;
628              
629 56836         85515 my $old_spec_version = $state->{specification_version};
630 56836         97360 my $error_count = $state->{errors}->@*;
631              
632 56836 100       174520 if (not $sub->($vocabulary, $schema, $state)) {
633             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
634 243 50       800 if $error_count == $state->{errors}->@*;
635 243         325 $valid = 0;
636 243         903 next;
637             }
638             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
639 56593 50       188077 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       116241 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
643              
644 56593 100       224009 if (my $callback = $state->{callbacks}{$keyword}) {
645 4         6 $error_count = $state->{errors}->@*;
646              
647 4 50       13 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       2114 if $error_count != $state->{errors}->@*;
655             }
656             }
657             }
658              
659 33246         72198 delete $state->{keyword};
660              
661 33246 100 100     98160 if ($self->strict and keys %unknown_keywords) {
662 2 50       17 $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         176222 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
668 104050 100       175084 next if not exists $schema->{$keyword};
669 224         932 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
670             .canonical_uri($state).'")';
671 224 50       26500 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
672 224         1184 my @list = map '"'.$_.'"', @$alternates;
673 224 50       701 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
674 224 100       757 splice(@list, -1, 0, 'or') if @list > 1;
675 224         848 $message .= ': this should be rewritten as '.join(' ', @list);
676             }
677 224         57782 carp $message;
678             }
679              
680 33246         149987 return $valid;
681             }
682              
683 34834     34835   47987 sub _evaluate_subschema ($self, $data, $schema, $state) {
  34834         46621  
  34834         46528  
  34834         40977  
  34834         41179  
  34834         44827  
684 34834 50       66399 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     133495 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
689 34834         293149 delete $state->@{'keyword', grep /^_/, keys %$state};
690              
691             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
692 34834 100       147450 if $state->{depth}++ > $self->max_traversal_depth;
693              
694 34831         105469 my $schema_type = get_type($schema);
695 34831 100 66     71909 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       63022 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
699              
700 33987 100       84167 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         88867 my $canonical_uri = canonical_uri($state);
705 33349         92239 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
706             {
707 47     47   49585 use autovivification qw(fetch store);
  47         87  
  47         213  
  33349         39798  
708             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
709             if grep substr($schema_location, 0, length) eq $_,
710 33349 100       128505 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
711 33348         4157169 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
712             }
713              
714 33348         3318602 my $valid = 1;
715 33348         222710 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         80673 my $parent_annotations = $state->{annotations};
719 33348         66079 $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     257499 && (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         63644 my $defaults = $state->{defaults};
732 33348 100       74375 $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         104060 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
741 183051         302044 my $vocabulary = $state->{vocabularies}[$vocab_index];
742 183051         182130 my $keyword_list;
743              
744 183051   66     278652 for (my $keyword_index = 0;
745             $keyword_index < ($keyword_list //= do {
746 47     47   14731 use autovivification qw(fetch store);
  47         97  
  47         173  
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     619038 ];
751             })->@*;
752             $keyword_index++) {
753 1510156         2098299 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
754 1510156 100       3249264 next if not exists $schema->{$keyword};
755              
756             # keywords adjacent to $ref are not evaluated before draft2019-09
757 60037 100 100     212579 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
758              
759 60006         114519 delete $unknown_keywords{$keyword};
760 60006 100 100     123440 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
761              
762 60005         126120 $state->{keyword} = $keyword;
763              
764 60005 100       97689 if ($sub) {
765 55929         87499 my $old_spec_version = $state->{specification_version};
766 55929         97036 my $error_count = $state->{errors}->@*;
767              
768 55929         87302 try {
769 55929 100       184636 if (not $sub->($vocabulary, $data, $schema, $state)) {
770             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
771 13161 50       41768 if $error_count == $state->{errors}->@*;
772 13161         18033 $valid = 0;
773              
774 13161 100 100     63926 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
775 6948         29187 next;
776             }
777              
778             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
779 42724 50       308631 if $error_count != $state->{errors}->@*;
780             }
781             catch ($e) {
782 44 100       622 die $e if $e->$_isa('JSON::Schema::Modern::Error');
783 2         26 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       99595 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
788             }
789              
790 46800 100 100     189788 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
791 24         39 my $error_count = $state->{errors}->@*;
792              
793 24 100       83 if (not $callback->($data, $schema, $state)) {
794             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
795 2 50       10 if $error_count == $state->{errors}->@*;
796 2         2 $valid = 0;
797              
798 2 100 66     12 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       569 if $error_count != $state->{errors}->@*;
803             }
804             }
805             }
806              
807 33304         75004 delete $state->{keyword};
808              
809 33304 100 100     73431 if ($state->{strict} and keys %unknown_keywords) {
810 3 100       43 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     77157 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         1090 foreach grep !/^x-/, keys %$data;
820              
821             my @evaluated_properties = map {
822 156         462 my $keyword = $_->{keyword};
  577         858  
823             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
824 577 100       1454 ? $_->{annotation}->@* : ();
825             } local_annotations($state);
826              
827             # tick off properties that were recognized by this subschema
828 156         403 $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     462 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
833             }
834              
835 33301 100 100     102470 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         2700 foreach sort keys %unknown_keywords;
838             }
839              
840             # only keep new annotations if schema is valid
841 33301 100       80998 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     86525 if $valid and $state->{defaults};
846              
847 33301         237276 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   111961 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
870 44035   100     2780388 ($_[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   65667 use autovivification 'store';
  47         99  
  47         234  
876             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
877 103     104   1114 foreach pairs @_[1..$#_];
878             }
879 25   50 26   11118 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
880 17395   100 17396   2931156 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
881 2123   50 2124   248042 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
882              
883 18976     18977   48182 sub _add_resource ($self, @kvs) {
  18976         31144  
  18976         37130  
  18976         25951  
884 18976         82028 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
885 18976         38494 my ($canonical_uri, $resource) = @$pair;
886              
887 18976 100       50403 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       51931 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       6139 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
896 859 100       247730 next if not @diffs;
897 10         2515 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         648 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
903             }
904              
905 47     47   24298 use autovivification 'store';
  47         97  
  47         183  
906 18115         84378 $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   1022 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   31  
  12     13   208  
  12     12   468  
  12     12   21  
  12     12   176  
  12     12   595  
  12     12   19  
  12     7   167  
  12     7   649  
  12     7   19  
  12     7   192  
  12     7   516  
  12     7   32  
  12     7   158  
  12     7   433  
  12     1   21  
  12     1   169  
  12     1   44  
  12     1   33  
  12     1   127  
  12     1   34  
  12         17  
  12         33  
  7         42  
  7         14  
  7         130  
  7         25  
  7         15  
  7         72  
  7         20  
  7         14  
  7         74  
  7         22  
  7         16  
  7         110  
  7         28  
  7         14  
  7         108  
  7         24  
  7         12  
  7         77  
  7         22  
  7         10  
  7         74  
  7         28  
  7         12  
  7         68  
  1         4  
  1         1  
  1         9  
  1         4  
  1         2  
  1         9  
  1         4  
  1         1  
  1         8  
  1         3  
  1         2  
  1         8  
  1         3  
  1         1  
  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   2046 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
931              
932 10     11 1 20539 sub add_vocabulary ($self, $classname) {
  10         19  
  10         18  
  10         15  
933 10 50       244 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
934              
935 10         937 $vocabulary_class_type->(load_module($classname));
936              
937             # uri => version, uri => version
938 7         656 foreach my $pair (pairs $classname->vocabulary) {
939 7         107 my ($uri_string, $spec_version) = @$pair;
940 7         39 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
941 6         8955 $spec_version_type->($spec_version);
942              
943 4 100       588 croak 'keywords starting with "$" are reserved for core and cannot be used'
944             if grep /^\$/, $classname->keywords;
945              
946 3         29 $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   21127 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   125  
  38     38   1104  
  38     38   17917  
  38     38   169  
  38     38   1336  
  38     38   26525  
  38     30   195  
  38     30   1232  
  38     30   18912  
  38     30   170  
  38     30   1065  
  38     30   17776  
  38     30   150  
  38     19   1119  
  38     19   16298  
  38     19   128  
  38     19   1083  
  38     19   224  
  38     19   61  
  38     19   296  
  30     17   190  
  30     16   53  
  30     16   504  
  30     16   121  
  30     16   45  
  30     16   464  
  30     16   111  
  30     13   3236  
  30     13   389  
  30     13   92  
  30     13   56  
  30     13   385  
  30     13   97  
  30     13   47  
  30     12   4345  
  30     12   100  
  30     12   43  
  30     12   368  
  30     12   98  
  30     12   42  
  30     12   134  
  19     11   167  
  19     11   34  
  19     11   319  
  19     11   66  
  19     11   28  
  19     11   296  
  19     11   77  
  19     10   31  
  19     10   232  
  19     10   78  
  19     9   31  
  19     9   189  
  19     9   57  
  19     9   28  
  19     9   242  
  19     9   65  
  19     9   75  
  19     9   183  
  19     9   56  
  19     9   37  
  19     9   83  
  17     9   97  
  17     9   47  
  17     9   215  
  16     9   55  
  16     9   25  
  16     9   215  
  16     9   77  
  16     8   28  
  16     8   176  
  16     8   63  
  16     8   33  
  16     8   227  
  16     8   50  
  16     8   22  
  16     6   205  
  16     6   52  
  16     6   28  
  16     6   194  
  16     6   49  
  16     6   30  
  16     6   75  
  13     6   77  
  13     6   26  
  13     6   229  
  13     6   47  
  13     6   21  
  13     6   127  
  13     6   63  
  13     6   23  
  13     6   213  
  13     6   46  
  13     5   20  
  13     5   158  
  13     5   43  
  13     5   21  
  13     5   174  
  13     5   44  
  13     5   3141  
  13     5   197  
  13     5   42  
  13     5   22  
  13     5   87  
  12     6   65  
  12     6   25  
  12     6   151  
  12     6   41  
  12     6   17  
  12     6   155  
  12     6   65  
  12     6   20  
  12     4   147  
  12     4   34  
  12     4   19  
  12     4   208  
  12     4   39  
  12     4   17  
  12     4   125  
  12     4   34  
  12     4   14  
  12     4   111  
  12     4   35  
  12     4   16  
  12     4   60  
  11         61  
  11         20  
  11         161  
  11         35  
  11         19  
  11         133  
  11         43  
  11         15  
  11         146  
  11         34  
  11         20  
  11         105  
  11         55  
  11         15  
  11         178  
  11         38  
  11         21  
  11         132  
  11         37  
  11         18  
  11         57  
  10         75  
  10         19  
  10         141  
  10         32  
  10         16  
  10         133  
  9         29  
  9         14  
  9         139  
  9         30  
  9         12  
  9         87  
  9         26  
  9         15  
  9         145  
  9         28  
  9         28  
  9         77  
  9         26  
  9         11  
  9         36  
  9         54  
  9         32  
  9         136  
  9         33  
  9         16  
  9         89  
  9         32  
  9         14  
  9         89  
  9         29  
  9         14  
  9         137  
  9         32  
  9         15  
  9         123  
  9         29  
  9         13  
  9         94  
  9         22  
  9         16  
  9         45  
  9         57  
  9         14  
  9         128  
  9         28  
  9         16  
  9         107  
  9         32  
  9         15  
  9         117  
  9         50  
  9         12  
  9         126  
  9         28  
  9         12  
  9         70  
  9         26  
  9         14  
  9         183  
  9         28  
  9         14  
  9         41  
  8         50  
  8         13  
  8         113  
  8         25  
  8         12  
  8         93  
  8         29  
  8         15  
  8         97  
  8         25  
  8         14  
  8         79  
  8         26  
  8         14  
  8         87  
  8         46  
  8         12  
  8         99  
  8         25  
  8         17  
  8         49  
  6         38  
  6         14  
  6         104  
  6         21  
  6         11  
  6         86  
  6         23  
  6         9  
  6         56  
  6         18  
  6         15  
  6         56  
  6         32  
  6         10  
  6         65  
  6         16  
  6         15  
  6         66  
  6         16  
  6         8  
  6         25  
  6         34  
  6         12  
  6         70  
  6         19  
  6         8  
  6         63  
  6         24  
  6         10  
  6         63  
  6         22  
  6         9  
  6         77  
  6         21  
  6         12  
  6         66  
  6         20  
  6         9  
  6         83  
  6         17  
  6         12  
  6         53  
  6         577  
  6         5959  
  6         139  
  6         553  
  6         4085  
  6         76  
  6         442  
  6         3731  
  6         62  
  5         18  
  5         9  
  5         79  
  5         16  
  5         7  
  5         71  
  5         18  
  5         7  
  5         50  
  5         15  
  5         9  
  5         28  
  5         31  
  5         10  
  5         69  
  5         18  
  5         9  
  5         47  
  5         43  
  5         10  
  5         57  
  5         17  
  5         9  
  5         76  
  5         17  
  5         8  
  5         78  
  5         23  
  5         9  
  5         71  
  5         16  
  5         8  
  5         26  
  6         35  
  6         10  
  6         69  
  6         18  
  6         9  
  6         84  
  6         20  
  6         11  
  6         69  
  6         19  
  6         9  
  6         69  
  6         15  
  6         8  
  6         57  
  6         18  
  6         7  
  6         61  
  6         18  
  6         7  
  6         86  
  6         28  
  6         11  
  6         59  
  4         12  
  4         6  
  4         29  
  4         16  
  4         7  
  4         38  
  4         11  
  4         6  
  4         49  
  4         12  
  4         5  
  4         30  
  4         10  
  4         6  
  4         49  
  4         10  
  4         6  
  4         20  
  4         21  
  4         5  
  4         45  
  4         12  
  4         8  
  4         29  
  4         13  
  4         8  
  4         41  
  4         11  
  4         7  
  4         61  
  4         13  
  4         5  
  4         53  
  4         12  
  4         8  
  4         42  
  4         8  
  4         281  
  4         24  
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   652122 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
977 5924     5925   34308 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
978 4     5   228 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   98 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         87  
  53         84  
  53         72  
  53         95  
982 53 100       250 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         8 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
986 2         7 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
987             }
988              
989 51         95 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       185 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
993 51 100       215 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
994              
995 51 100       149 return (undef, []) if not $valid;
996              
997 49         94 my @vocabulary_classes;
998              
999 49         304 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
1000 105         268 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     10475 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1003              
1004 97 100       874 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1005              
1006 89         193 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       319 if $spec_version ne $schema_info->{specification_version};
1010              
1011 83         209 push @vocabulary_classes, $class;
1012             }
1013              
1014             @vocabulary_classes = sort {
1015 49 50       210 $a->evaluation_order <=> $b->evaluation_order
  49 50       191  
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     223 $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         86 my %seen_keyword;
1024 49         97 foreach my $class (@vocabulary_classes) {
1025 83         368 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       1175 if $seen_keyword{$keyword};
1029 794         1593 $seen_keyword{$keyword} = $class;
1030             }
1031             }
1032              
1033 49 100       589 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         6088 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   66631 };
  47         90  
1045              
1046             # for internal use only. files are under share/
1047 47         49745 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   402 };
  47         101  
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   930433 sub _get_or_load_resource ($self, $uri) {
  24428         37553  
  24428         30486  
  24428         28862  
1085 24428         60858 my $resource = $self->_get_resource($uri);
1086 24428 100       2144100 return $resource if $resource;
1087              
1088 114 100       513 if (my $document = load_cached_document($self, $uri)) {
1089 103         442 return $self->_get_resource($uri);
1090             }
1091              
1092             # TODO:
1093             # - load from network or disk
1094              
1095 11         26 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   237503 sub _fetch_from_uri ($self, $uri_reference) {
  24190         35147  
  24190         33135  
  24190         29029  
1107 24190 50       87848 $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         5986914 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1111 24190 100       115422 return if not $resource;
1112              
1113 24179         49932 my $fragment = $uri_reference->fragment;
1114 24179 100 100     123615 if (not length($fragment) or $fragment =~ m{^/}) {
1115 23599   100     191844 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1116 23599 100       180409 return if not defined $subschema;
1117              
1118 23597         32593 my $closest_resource;
1119 23597 100       41724 if (not length $fragment) { # we already have the canonical resource root
1120 21474         42310 $closest_resource = [ undef, $resource ];
1121             }
1122             else {
1123             # determine the canonical uri by finding the closest schema resource(s)
1124 2123         5668 my $doc_addr = refaddr($resource->{document});
1125             my @closest_resources =
1126 501         2989 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1127             grep { !length($_->[1]{path}) # document root
1128 2913 100 66     28130 || length($document_path)
1129             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1130 2123         6891 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228352         366923  
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       38286 if (@closest_resources > 1) {
1135             # find the resource key that most closely matches the original query uri, by matching prefixes
1136 464         2063 my $match = $uri_reference.'';
1137             @closest_resources =
1138 26         91 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         85766 @closest_resources;
1141             }
1142              
1143 2123         4535 $closest_resource = $closest_resources[0];
1144             }
1145              
1146             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1147 23597         100287 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1148 23597 100       1599032 $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         361495 $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     3595 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         3104 $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   63 sub _prefix_match_length ($x, $y) {
  52         83  
  52         89  
  52         64  
1171 52         119 my $len = min(length($x), length($y));
1172 52         102 foreach my $pos (0..$len) {
1173 1456 100       2153 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         740 Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION('4.38'); 1 } ? 'Cpanel::JSON::XS'
  47         61715  
1184 47 0       96 : eval { JSON::PP->VERSION('4.11'); 1 } ? 'JSON::PP'
  2 50       3  
  2         31  
1185 47     47   344 : die 'Cpanel::JSON::XS 4.38 or JSON::PP 4.11 is required';
  47         84  
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 2669 sub add_media_type { $media_type_type->({ @_[1..2] }); $_[0]->__media_type->{$_[1]} = $_[2]; }
  4         279  
1232              
1233             # get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
1234 39     40 1 10042 sub get_media_type ($self, $type) {
  39         63  
  39         62  
  39         38  
1235 39         855 my $types = $self->__media_type;
1236 39         731 my $mt = $types->{fc $type};
1237 39 100       124 return $mt if $mt;
1238              
1239 9 100 100 53   62 return $types->{(first { m{([^/]+)/\*\z} && fc($type) =~ m{^\Q$1\E/[^/]+\z} } keys %$types) // '*/*'};
  52         325  
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 595 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 640 sub FREEZE ($self, $serializer) {
  3         4  
  3         6  
  3         3  
1269 3         29 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         14 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1273 3         21 return $data;
1274             }
1275              
1276             # callback hook for Sereal::Decoder
1277 4     5 0 183 sub THAW ($class, $serializer, $data) {
  4         8  
  4         7  
  4         118  
  4         6  
1278 4         8 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         17 foreach uniq(
1283             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1284             (map $_->[1], values $self->__vocabulary_classes->%*));
1285              
1286 4         88 return $self;
1287             }
1288              
1289             1;
1290              
1291             __END__