File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 1014 1028 98.6
branch 263 320 82.1
condition 161 186 86.5
subroutine 229 229 100.0
pod 14 16 87.5
total 1681 1779 94.4


line stmt bran cond sub pod time code
1 50     50   19458308 use strict;
  50         131  
  50         2254  
2 50     50   324 use warnings;
  50         111  
  50         5338  
3             package JSON::Schema::Modern; # git description: v0.631-7-g486a9f1e
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.632';
9              
10 50     50   1121 use 5.020; # for fc, unicode_strings features
  50         227  
11 50     50   30234 use Moo;
  50         427472  
  50         322  
12 50     50   84519 use strictures 2;
  50         503  
  50         2308  
13 50     50   24461 use stable 0.031 'postderef';
  50         991  
  50         422  
14 50     50   11033 use experimental 'signatures';
  50         159  
  50         268  
15 50     50   3204 no autovivification warn => qw(fetch store exists delete);
  50         176  
  50         523  
16 50     49   4585 use if "$]" >= 5.022, experimental => 're_strict';
  49         139  
  49         1692  
17 49     49   5204 no if "$]" >= 5.031009, feature => 'indirect';
  49         125  
  49         3934  
18 49     49   421 no if "$]" >= 5.033001, feature => 'multidimensional';
  49         106  
  49         3273  
19 49     49   423 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  49         168  
  49         3380  
20 49     49   403 no if "$]" >= 5.041009, feature => 'smartmatch';
  49         140  
  49         2574  
21 49     49   568 no feature 'switch';
  49         232  
  49         1770  
22 49     49   28754 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  49         11087410  
  49         2574  
23 49     49   530 use Carp qw(croak carp);
  49         380  
  49         4327  
24 49     48   509 use List::Util 1.55 qw(pairs first uniqint pairmap uniq min);
  48         1582  
  47         5722  
25 47     48   360 use if "$]" < 5.041010, 'List::Util' => 'any';
  48         673  
  48         2849  
26 48     48   305 use if "$]" >= 5.041010, experimental => 'keyword_any';
  48         492  
  47         1214  
27 47     48   32575 use builtin::compat qw(refaddr load_module);
  48         981313  
  48         4334  
28 48     48   35949 use Mojo::URL;
  48         541914  
  48         5159  
29 48     48   31332 use Safe::Isa;
  48         34444  
  48         16579  
30 48     48   25769 use Mojo::File 'path';
  48         993676  
  48         8086  
31 48     47   499 use Storable 'dclone';
  47         115  
  47         3980  
32 47     47   339 use File::ShareDir 'dist_dir';
  47         99  
  47         3530  
33 47     47   26013 use MooX::TypeTiny 0.002002;
  47         25998  
  47         278  
34 47     47   424919 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  47         6364794  
  47         744  
35 47     47   251587 use Digest::MD5 'md5';
  47         125  
  47         3630  
36 47     47   28696 use Feature::Compat::Try;
  47         20034  
  47         384  
37 47     47   28413 use JSON::Schema::Modern::Error;
  47         1179  
  47         2356  
38 47     47   31071 use JSON::Schema::Modern::Result;
  47         703  
  47         2403  
39 47     47   30367 use JSON::Schema::Modern::Document;
  47         1210  
  47         1006  
40 47     47   3946 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 load_cached_document);
  47         126  
  47         6313  
41 47     47   463 use namespace::clean;
  47         145  
  47         570  
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   40051 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  47         122  
  47         4314  
52 47     47   318 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  47         120  
  47         45706  
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             # Validation §7.1-2: "Note that the "type" keyword in this specification defines an "integer" type
106             # which is not part of the data model. Therefore a format attribute can be limited to numbers, but
107             # not specifically to integers."
108             my $core_types = Enum[qw(null object array boolean string number)];
109             my @core_formats = qw(date-time date time duration email idn-email hostname idn-hostname ipv4 ipv6 uri uri-reference iri iri-reference uuid uri-template json-pointer relative-json-pointer regex);
110              
111             # { $format_name => { type => ..., sub => ... }, ... }
112             has _format_validations => (
113             is => 'bare',
114             isa => my $format_type = HashRef[Dict[
115             type => $core_types|ArrayRef[$core_types],
116             sub => CodeRef,
117             ]],
118             init_arg => 'format_validations',
119             );
120              
121 957   100 957   1764 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  957         1857  
  957         1806  
  957         1519  
  957         12071  
122              
123 12     12 1 36457 sub add_format_validation ($self, $format, $definition) {
  12         31  
  12         22  
  12         77  
  12         25  
124 12 50 100     95 return if exists(($self->{_format_validations}//{})->{$format});
125              
126 12 100       76 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
127 12         119 $format_type->({ $format => $definition });
128              
129             # all core formats are of type string (so far); changing type of custom format is permitted
130             croak "Type for override of format $format does not match original type"
131 5 100 100     404 if any { $format eq $_ } @core_formats and $definition->{type} ne 'string';
  66         709  
132              
133 47     47   479 use autovivification 'store';
  47         119  
  47         506  
134 4         20 $self->{_format_validations}{$format} = $definition;
135             }
136              
137             around BUILDARGS => sub ($orig, $class, @args) {
138             my $args = $class->$orig(@args);
139             croak 'output_format: strict_basic can only be used with specification_version: draft2019-09'
140             if ($args->{output_format}//'') eq 'strict_basic'
141             and ($args->{specification_version}//'') ne 'draft2019-09';
142              
143             croak 'collect_annotations cannot be used with specification_version '.$args->{specification_version}
144             if $args->{collect_annotations} and ($args->{specification_version}//'') =~ /^draft[467]\z/;
145              
146             $args->{format_validations} = +{
147             map +($_->[0] => ref $_->[1] eq 'HASH' ? $_->[1] : +{ type => 'string', sub => $_->[1] }),
148             pairs $args->{format_validations}->%*
149             } if $args->{format_validations};
150              
151             return $args;
152             };
153              
154             sub add_schema {
155 17381 50   17381 1 206858 croak 'insufficient arguments' if @_ < 2;
156 17381         35462 my $self = shift;
157              
158 17381 100       89970 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
159 2         288 Carp::carp('use of deprecated form of add_schema with document');
160 2         26 return $self->add_document($_[0]);
161             }
162              
163             # TODO: resolve $uri against $self->base_uri
164 17380 50       247656 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
165             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
166              
167 17380 100       335601 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
168 17379 50       127742 croak 'insufficient arguments' if not @_;
169              
170 17379 100       53311 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
171 2         405 Carp::carp('use of deprecated form of add_schema with document');
172 2         18 return $self->add_document($uri, $_[0]);
173             }
174              
175             # document BUILD will trigger $self->traverse($schema)
176             # Note we do not pass the uri to the document constructor, so resources in that document may still
177             # be relative
178 17378         698991 my $document = JSON::Schema::Modern::Document->new(
179             schema => $_[0],
180             evaluator => $self, # used mainly for traversal during document construction
181             );
182              
183             # try to reuse the same document, if the same schema is being added twice:
184             # this results in _add_resource silently ignoring the duplicate add, rather than erroring.
185 17378         475596 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
186 17378 100       1305210 if (my $existing_doc = first {
187 722237   66 722237   12579728 my $existing_checksum = $_->_checksum
188             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
189 722237 100       5135448 $existing_checksum eq $schema_checksum
190             and $_->canonical_uri eq $document->canonical_uri
191             # FIXME: must also check spec version/metaschema_uri/vocabularies
192             } uniqint map $_->{document}, $self->_canonical_resources) {
193 12293         3860413 $document = $existing_doc;
194             }
195              
196 17378         694025 $self->add_document($uri, $document);
197             }
198              
199             sub add_document {
200 18190 50   18190 1 164234 croak 'insufficient arguments' if @_ < 2;
201 18190         48257 my $self = shift;
202              
203             # TODO: resolve $uri against $self->base_uri
204 18190 50       92712 my $base_uri = !$_[0]->$_isa('JSON::Schema::Modern::Document') ? Mojo::URL->new(shift)
    100          
205             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
206              
207 18190 50       7384227 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
208 18190 50       140017 croak 'insufficient arguments' if not @_;
209              
210 18190         43213 my $document = shift;
211 18190 50       67906 croak 'wrong document type' if not $document->$_isa('JSON::Schema::Modern::Document');
212              
213             # we will never add a document to the resource index if it has errors
214 18190 100       320975 die JSON::Schema::Modern::Result->new(
215             output_format => $self->output_format,
216             valid => 0,
217             errors => [ $document->errors ],
218             exception => 1,
219             ) if $document->has_errors;
220              
221 18010 100       81053 if (not length $base_uri){
222 17177         2416284 foreach my $res_pair ($document->resource_pairs) {
223 18037         317770 my ($uri_string, $doc_resource) = @$res_pair;
224              
225             # this might croak if there are duplicates or malformed entries.
226 18037         200854 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
227             }
228              
229 17172         8928287 return $document;
230             }
231              
232 834         180665 my @root; # uri_string => resource hash of the resource at path ''
233              
234             # document resources are added after resolving each resource against our provided base uri
235 834         3891 foreach my $res_pair ($document->resource_pairs) {
236 864         2414 my ($uri_string, $doc_resource) = @$res_pair;
237 864         3128 $uri_string = Mojo::URL->new($uri_string)->to_abs($base_uri)->to_string;
238              
239             my $new_resource = {
240             canonical_uri => Mojo::URL->new($doc_resource->{canonical_uri})->to_abs($base_uri),
241 864         559469 $doc_resource->%{qw(path specification_version vocabularies)},
242             document => $document,
243             };
244              
245 864   100     465738 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
246 47     47   78760 use autovivification 'store';
  47         118  
  47         297  
247             $new_resource->{anchors}{$anchor} = {
248             $doc_resource->{anchors}{$anchor}->%{path},
249             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
250 170 100       2352 canonical_uri => Mojo::URL->new($doc_resource->{anchors}{$anchor}{canonical_uri})->to_abs($base_uri),
251             };
252             }
253              
254             # this might croak if there are duplicates or malformed entries.
255 864         86580 $self->_add_resource($uri_string => $new_resource);
256 857 100 66     461018 @root = ($uri_string => $new_resource) if $new_resource->{path} eq '' and $uri_string !~ /#./;
257             }
258              
259             # associate the root resource with the base uri we were provided, if it does not already exist
260 827 100       4822 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
261              
262 827         200347 return $document;
263             }
264              
265 4     4 1 14900 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         25  
  4         13  
  4         12  
  4         56  
  4         14  
266 4 50       140 croak 'evaluate_json_string called in void context' if not defined wantarray;
267              
268 4         23 my $data;
269 4         13 try {
270 4         141 $data = $self->_json_decoder->decode($json_data)
271             }
272             catch ($e) {
273 3         147 return JSON::Schema::Modern::Result->new(
274             output_format => $self->output_format,
275             valid => 0,
276             exception => 1,
277             errors => [
278             JSON::Schema::Modern::Error->new(
279             depth => 0,
280             mode => 'traverse',
281             keyword => undef,
282             keyword_location => '',
283             error => $e,
284             )
285             ],
286             );
287             }
288              
289 2         30 return $self->evaluate($data, $schema, $config_override);
290             }
291              
292             # this is called whenever we need to walk a document for something.
293             # for now it is just called when a ::Document object is created, to verify the integrity of the
294             # schema structure, to identify the metaschema (via the $schema keyword), and to extract all
295             # embedded resources via $id and $anchor keywords within.
296             # Returns the internal $state object accumulated during the traversal.
297 17930     17930 1 107070 sub traverse ($self, $schema_reference, $config_override = {}) {
  17930         35067  
  17930         35814  
  17930         37237  
  17930         30877  
298 17930         72889 my %overrides = %$config_override;
299 17930         89550 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
300 17930 50       63853 croak join(', ', sort keys %overrides), ' not supported as a config override in traverse'
301             if keys %overrides;
302              
303             # Note: the starting position is not guaranteed to be at the root of the $document,
304             # nor is the fragment portion of this uri necessarily empty
305 17930   66     120288 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
306 17930   100     7567806 my $initial_path = $config_override->{traversed_keyword_path} // '';
307 17930   100     168431 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
308              
309 17930 50       110168 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
310              
311 17930 100       67321 if (length(my $uri_path = $initial_uri->fragment)) {
312 5 50       57 croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
313              
314 5 50       30 croak 'traversed_keyword_path does not match initial_schema_uri path fragment'
315             if substr($initial_path, -length($uri_path)) ne $uri_path;
316             }
317              
318             my $state = {
319             depth => 0,
320             data_path => '', # this never changes since we don't have an instance yet
321             initial_schema_uri => $initial_uri, # the canonical URI as of the start of this method or last $id
322             traversed_keyword_path => $initial_path, # the accumulated traversal path as of the start or last $id
323             keyword_path => '', # the rest of the path, since the start of this method or last $id
324             specification_version => $spec_version,
325             errors => [],
326             identifiers => {},
327             subschemas => [],
328             $config_override->{skip_ref_checks} ? () : (references => []),
329             callbacks => $config_override->{callbacks} // {},
330 17930 100 100     455866 evaluator => $self,
331             traverse => 1,
332             };
333              
334 17930         55017 my $valid = 1;
335              
336 17930         53837 try {
337             # determine the initial value of specification_version and vocabularies, so we have something to start
338             # with in _traverse_subschema().
339             # a subsequent "$schema" keyword can still change these values, and it is always processed
340             # first, so the override is skipped if the keyword exists in the schema
341             $state->{metaschema_uri} =
342             (ref $schema_reference eq 'HASH' && exists $schema_reference->{'$schema'} ? undef
343 17930 100 100     357134 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
344              
345 17930 100       95495 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
346 17924         634450 $state->@{qw(specification_version vocabularies)} = @$metaschema_info;
347             }
348             else {
349             # metaschema has not been processed for vocabularies yet...
350              
351             die 'something went wrong - cannot get metaschema data for '.$state->{metaschema_uri}
352 7 50       2173 if not $config_override->{metaschema_uri};
353              
354             # use the Core vocabulary to set metaschema info via the '$schema' keyword implementation
355             $valid = $self->_get_metaschema_vocabulary_classes($self->METASCHEMA_URIS->{$spec_version})->[1][0]
356 7         85 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
357             }
358              
359 17930 100 66     203329 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
360 17930 50 66     61922 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
361 17930 50 66     168176 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
362             }
363             catch ($e) {
364 1 0       5 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    0          
365 1         2 push $state->{errors}->@*, $e->errors;
366             }
367             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
368             # note: we should never be here, since traversal subs are no longer fatal
369 1         20 push $state->{errors}->@*, $e;
370             }
371             else {
372 1         5 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
373             }
374             }
375              
376 17930         106077 return $state;
377             }
378              
379             # the actual runtime evaluation of the schema against input data.
380 17376     17376 1 33491033 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17376         45250  
  17376         41878  
  17376         36417  
  17376         47386  
  17376         39999  
381 17376 50       74501 croak 'evaluate called in void context' if not defined wantarray;
382              
383 17376         60934 my %overrides = %$config_override;
384 17376         84355 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)};
385 17376 50       68128 croak join(', ', sort keys %overrides), ' not supported as a config override in evaluate'
386             if keys %overrides;
387              
388             my $state = {
389             data_path => $config_override->{data_path} // '',
390 17375   100     244635 traversed_keyword_path => $config_override->{traversed_keyword_path} // '', # the accumulated path as of the start of evaluation or last $id or $ref
      100        
391             initial_schema_uri => Mojo::URL->new, # the canonical URI as of the start of evaluation or last $id or $ref
392             keyword_path => '', # the rest of the path, since the start of evaluation or last $id or $ref
393             errors => [],
394             depth => 0,
395             };
396              
397 17375         347954 my $valid;
398 17375         44897 try {
399 17375 100 100     88064 if (is_schema($schema_reference)) {
    100          
400             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
401 17301         79990 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
402             }
403             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
404 4         37 abort($state, 'invalid schema type: %s', get_type($schema_reference));
405             }
406              
407 17186         243531 my $schema_info = $self->_fetch_from_uri($schema_reference);
408 17186 100       67990 abort($state, 'EXCEPTION: unable to find resource "%s"', $schema_reference)
409             if not $schema_info;
410              
411             abort($state, 'EXCEPTION: "%s" is not a schema', $schema_reference)
412 17180 100       120785 if not $schema_info->{document}->get_entity_at_location($schema_info->{document_path});
413              
414             $state = +{
415             %$state,
416             initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
417             $schema_info->%{qw(document specification_version vocabularies)},
418             dynamic_scope => [ $schema_info->{canonical_uri}->clone->fragment(undef) ],
419             annotations => [],
420             seen => {},
421             callbacks => $config_override->{callbacks} // {},
422             evaluator => $self,
423             (map {
424 120253   100     3248058 my $val = $config_override->{$_} // $self->$_;
425 120253 100       1164809 defined $val ? ($_ => $val) : ()
426             # note: this is a subset of the allowed overrides defined above
427             } qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
428 17179 100 100     189668 $config_override->{with_defaults} // $self->with_defaults ? (defaults => {}) : (),
      66        
429             };
430              
431             # this hash will be added to at each level of schema evaluation
432 17179 100       109566 $state->{seen_data_properties} = {} if $config_override->{_strict_schema_data};
433              
434             # we're going to set collect_annotations during evaluation when we see an unevaluated* keyword
435             # (or for object data when the _strict_schema_data configuration is set),
436             # but after we pass to a new data scope we'll clear it again.. unless we've got the config set
437             # globally for the entire evaluation, so we store that value in a high bit.
438 17179   100     101626 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
439              
440 17179         112521 $valid = $self->_eval_subschema($data, $schema_info->{schema}, $state);
441 17156 50 66     92340 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
442 17156 50 66     165639 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
443             }
444             catch ($e) {
445 219 100       1512 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
446 180         4501 return $e;
447             }
448             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
449 34         915 push $state->{errors}->@*, $e;
450             }
451             else {
452 5         138 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
453             }
454             }
455              
456 17195 100       77941 if ($state->{seen_data_properties}) {
457 5         14 my %unknown_keywords;
458 5         79 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
459             keys $state->{seen_data_properties}->%*) {
460 15         83 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
461 15   100     77 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
462             }
463              
464 5         28 foreach my $parent (sort keys %unknown_keywords) {
465             $valid = E({ %$state, data_path => $parent },
466             'unknown keyword%s seen in schema: %s', $unknown_keywords{$parent}->@* > 1 ? 's' : '',
467 8 100       183 join(', ', sort $unknown_keywords{$parent}->@*));
468             }
469             }
470              
471 17195 50 50     119692 die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
472              
473             return JSON::Schema::Modern::Result->new(
474             output_format => $self->output_format,
475             valid => $valid,
476             $valid
477             # strip annotations from result if user didn't explicitly ask for them
478             ? ($config_override->{collect_annotations} // $self->collect_annotations
479             ? (annotations => $state->{annotations}) : ())
480             : (errors => $state->{errors}),
481 17195 100 100     825279 $state->{defaults} ? (defaults => $state->{defaults}) : (),
    100          
    100          
482             );
483             }
484              
485 10     11 1 46153 sub validate_schema ($self, $schema, $config_override = {}) {
  10         24  
  10         20  
  10         27  
  10         19  
486 10 50       46 croak 'validate_schema called in void context' if not defined wantarray;
487              
488             my $metaschema_uri = ref $schema eq 'HASH' && $schema->{'$schema'} ? $schema->{'$schema'}
489 10 100 66     143 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
490              
491             my $result = $self->evaluate($schema, $metaschema_uri,
492 10 100 100     125 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
493              
494 10 100       288 return $result if not $result->valid;
495              
496             # the traversal pass will validate all constraints that weren't handled by the metaschema
497 3         123 my $state = $self->traverse($schema);
498             return JSON::Schema::Modern::Result->new(
499             output_format => $self->output_format,
500             valid => 0,
501             errors => $state->{errors},
502 3 100       43 ) if $state->{errors}->@*;
503              
504 2         47 return $result; # valid: true
505             }
506              
507 8     9 1 38010 sub get ($self, $uri_reference) {
  8         19  
  8         15  
  8         18  
508 8 100       26 if (wantarray) {
509 5         22 my $schema_info = $self->_fetch_from_uri($uri_reference);
510 5 100       27 return if not $schema_info;
511 4 100       650 my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
512 4         45 return ($subschema, $schema_info->{canonical_uri});
513             }
514             else { # abridged version of _fetch_from_uri
515 3 50       28 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
516 3         609 my $fragment = $uri_reference->fragment;
517 3         21 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
518 3 50       135 return if not $resource;
519              
520 3         6 my $schema;
521 3 100 100     22 if (not length($fragment) or $fragment =~ m{^/}) {
522 2   100     122 $schema = $resource->{document}->get($resource->{path}.($fragment//''));
523             }
524             else { # we are following a URI with a plain-name fragment
525 1 50 50     24 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
526 0         0 $schema = $resource->{document}->get($subresource->{path});
527             }
528 2 100       209 return ref $schema ? dclone($schema) : $schema;
529             }
530             }
531              
532 0     1 1 0 sub get_document ($self, $uri_reference) {
  0         0  
  0         0  
  0         0  
533 0         0 my $schema_info = $self->_fetch_from_uri($uri_reference);
534 0 0       0 return if not $schema_info;
535 0         0 return $schema_info->{document};
536             }
537              
538             # defined lower down:
539             # sub add_media_type ($self, $media_type, $sub) { ... }
540             # sub get_media_type ($self, $media_type) { ... }
541             # sub add_encoding ($self, $encoding, $sub) { ... }
542             # sub get_encoding ($self, $encoding) { ... }
543             # sub add_vocabulary ($self, $classname) { ... }
544              
545             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
546              
547             # current spec version => { keyword => undef, or arrayref of alternatives }
548             my %removed_keywords = (
549             'draft4' => {
550             },
551             'draft6' => {
552             id => [ '$id' ],
553             },
554             'draft7' => {
555             id => [ '$id' ],
556             },
557             'draft2019-09' => {
558             id => [ '$id' ],
559             definitions => [ '$defs' ],
560             dependencies => [ qw(dependentSchemas dependentRequired) ],
561             },
562             'draft2020-12' => {
563             id => [ '$id' ],
564             definitions => [ '$defs' ],
565             dependencies => [ qw(dependentSchemas dependentRequired) ],
566             '$recursiveAnchor' => [ '$dynamicAnchor' ],
567             '$recursiveRef' => [ '$dynamicRef' ],
568             additionalItems => [ 'items' ],
569             },
570             );
571              
572             # {
573             # $spec_version => {
574             # $vocabulary_class => {
575             # traverse => [ [ $keyword => $subref ], [ ... ] ],
576             # evaluate => [ [ $keyword => $subref ], [ ... ] ],
577             # }
578             # }
579             # }
580             # If we could serialize coderefs, this could be an object attribute;
581             # otherwise, we might as well persist this for the lifetime of the process.
582             our $vocabulary_cache = {};
583              
584 42150     42151   93836 sub _traverse_subschema ($self, $schema, $state) {
  42150         76684  
  42150         76022  
  42150         71909  
  42150         75902  
585 42150         500781 delete $state->@{'keyword', grep /^_/, keys %$state};
586              
587             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
588 42150 50       273098 if $state->{depth}++ > $self->max_traversal_depth;
589              
590 42150         248745 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
591              
592 42150         184357 my $schema_type = get_type($schema);
593             return 1 if $schema_type eq 'boolean'
594             and ($state->{specification_version} ne 'draft4'
595 42150 100 100     204895 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
596              
597 34439 100       94763 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
598              
599 34422 100       139231 return 1 if not keys %$schema;
600              
601 33224         67640 my $valid = 1;
602 33224         268335 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
603              
604             # we use an index rather than iterating through the lists directly because the lists of
605             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
606             # and $schema keyword can make this change, and they both come first, therefore a simple index
607             # into the list is sufficient.
608             ALL_KEYWORDS:
609 33224         178827 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
610 203199         495284 my $vocabulary = $state->{vocabularies}[$vocab_index];
611 203199         286588 my $keyword_list;
612              
613 203199   66     464923 for (my $keyword_index = 0;
614             $keyword_index < ($keyword_list //= do {
615 47     47   220865 use autovivification qw(fetch store);
  47         128  
  47         296  
616             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{traverse} //= [
617             map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
618             $vocabulary->keywords($state->{specification_version})
619 203267   100     1012364 ];
620             })->@*;
621             $keyword_index++) {
622 1683582         3455415 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
623 1683582 100       5558973 next if not exists $schema->{$keyword};
624              
625             # keywords adjacent to $ref are not evaluated before draft2019-09
626 56825 100 100     324859 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
627              
628 56789         141294 delete $unknown_keywords{$keyword};
629 56789         156358 $state->{keyword} = $keyword;
630              
631 56789         132751 my $old_spec_version = $state->{specification_version};
632 56789         140893 my $error_count = $state->{errors}->@*;
633              
634 56789 100       253620 if (not $sub->($vocabulary, $schema, $state)) {
635             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
636 243 50       1167 if $error_count == $state->{errors}->@*;
637 243         489 $valid = 0;
638 243         1317 next;
639             }
640             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
641 56546 50       300593 if $error_count != $state->{errors}->@*;
642              
643             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
644 56546 100       172733 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
645              
646 56546 100       341748 if (my $callback = $state->{callbacks}{$keyword}) {
647 4         12 $error_count = $state->{errors}->@*;
648              
649 4 50       20 if (not $callback->($schema, $state)) {
650             die 'callback result is false but there are no errors (keyword: '.$keyword.')'
651 0 0       0 if $error_count == $state->{errors}->@*;
652 0         0 $valid = 0;
653 0         0 next;
654             }
655             die 'callback result is true but there are errors (keyword: '.$keyword.')'
656 4 50       4015 if $error_count != $state->{errors}->@*;
657             }
658             }
659             }
660              
661 33224         115085 delete $state->{keyword};
662              
663 33224 100 100     154660 if ($self->strict and keys %unknown_keywords) {
664 2 50       22 $valid = E($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
665             join(', ', sort keys %unknown_keywords));
666             }
667              
668             # check for previously-supported but now removed keywords
669 33224         271238 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
670 103924 100       262760 next if not exists $schema->{$keyword};
671 224         1208 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
672             .canonical_uri($state).'")';
673 224 50       33526 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
674 224         1431 my @list = map '"'.$_.'"', @$alternates;
675 224 50       837 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
676 224 100       1012 splice(@list, -1, 0, 'or') if @list > 1;
677 224         866 $message .= ': this should be rewritten as '.join(' ', @list);
678             }
679 224         74847 carp $message;
680             }
681              
682 33224         246236 return $valid;
683             }
684              
685 34815     34816   70649 sub _eval_subschema ($self, $data, $schema, $state) {
  34815         65176  
  34815         71054  
  34815         61824  
  34815         59144  
  34815         54367  
686 34815 50       94381 croak '_eval_subschema called in void context' if not defined wantarray;
687              
688             # callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
689             # paths; but annotations, errors are arrayrefs so their contents will be shared
690 34815   100     199398 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
691 34815         492629 delete $state->@{'keyword', grep /^_/, keys %$state};
692              
693             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
694 34815 100       222803 if $state->{depth}++ > $self->max_traversal_depth;
695              
696 34812         156220 my $schema_type = get_type($schema);
697 34812 100 66     109143 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
698              
699             # this should never happen, due to checks in traverse
700 33968 50       96450 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
701              
702 33968 100       137248 return 1 if not keys %$schema;
703              
704             # find all schema locations in effect at this data path + uri combination
705             # if any of them are absolute prefix of this schema location, we are in a loop.
706 33331         126302 my $canonical_uri = canonical_uri($state);
707 33331         146411 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
708             {
709 47     47   73494 use autovivification qw(fetch store);
  47         164  
  47         288  
  33331         56805  
710             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
711             if grep substr($schema_location, 0, length) eq $_,
712 33331 100       222421 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
713 33330         6245386 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
714             }
715              
716 33330         5379540 my $valid = 1;
717 33330         337767 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
718              
719             # set aside annotations collected so far; they are not used in the current scope's evaluation
720 33330         127762 my $parent_annotations = $state->{annotations};
721 33330         104482 $state->{annotations} = [];
722              
723             # in order to collect annotations from applicator keywords only when needed, we twiddle the low
724             # bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
725             # We also set it when _strict_schema_data is set, but only for object data instances.
726             $state->{collect_annotations} |=
727             0+((ref $data eq 'ARRAY' && exists $schema->{unevaluatedItems})
728             || ((my $is_object_data = ref $data eq 'HASH')
729 33330   100     395771 && (exists $schema->{unevaluatedProperties} || !!$state->{seen_data_properties})));
730              
731             # set aside defaults collected so far; we need to keep the subschema's defaults separated in
732             # case they must be discarded due to overall invalidity of the subschema
733 33330         102268 my $defaults = $state->{defaults};
734 33330 100       105153 $state->{defaults} = {} if $state->{defaults};
735              
736             # we use an index rather than iterating through the lists directly because the lists of
737             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
738             # and $schema keyword can make this change, and they both come first, therefore a simple index
739             # into the list is sufficient.
740              
741             ALL_KEYWORDS:
742 33330         159927 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
743 182959         444262 my $vocabulary = $state->{vocabularies}[$vocab_index];
744 182959         265445 my $keyword_list;
745              
746 182959   66     424234 for (my $keyword_index = 0;
747             $keyword_index < ($keyword_list //= do {
748 47     47   22179 use autovivification qw(fetch store);
  47         158  
  47         283  
749             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{evaluate} //= [
750             map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
751             $vocabulary->keywords($state->{specification_version})
752 182962   100     995752 ];
753             })->@*;
754             $keyword_index++) {
755 1509373         3102076 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
756 1509373 100       5031938 next if not exists $schema->{$keyword};
757              
758             # keywords adjacent to $ref are not evaluated before draft2019-09
759 60004 100 100     334039 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
760              
761 59973         153921 delete $unknown_keywords{$keyword};
762 59973 100 100     176029 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
763              
764 59972         179418 $state->{keyword} = $keyword;
765              
766 59972 100       136005 if ($sub) {
767 55896         139172 my $old_spec_version = $state->{specification_version};
768 55896         154003 my $error_count = $state->{errors}->@*;
769              
770 55896         133433 try {
771 55896 100       264322 if (not $sub->($vocabulary, $data, $schema, $state)) {
772             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
773 13143 50       63467 if $error_count == $state->{errors}->@*;
774 13143         24827 $valid = 0;
775              
776 13143 100 100     92425 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
777 6939         42078 next;
778             }
779              
780             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
781 42709 50       443564 if $error_count != $state->{errors}->@*;
782             }
783             catch ($e) {
784 44 100       961 die $e if $e->$_isa('JSON::Schema::Modern::Error');
785 2         32 abort($state, 'EXCEPTION: '.$e);
786             }
787              
788             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
789 42709 100       153803 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
790             }
791              
792 46785 100 100     291490 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
793 19         53 my $error_count = $state->{errors}->@*;
794              
795 19 100       89 if (not $callback->($data, $schema, $state)) {
796             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
797 2 50       12 if $error_count == $state->{errors}->@*;
798 2         7 $valid = 0;
799              
800 2 100 66     17 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
801 1         6 next;
802             }
803             warn 'callback result is true but there are errors (keyword: '.$keyword.')'
804 17 50       278 if $error_count != $state->{errors}->@*;
805             }
806             }
807             }
808              
809 33286         136241 delete $state->{keyword};
810              
811 33286 100 100     111659 if ($state->{strict} and keys %unknown_keywords) {
812 3 100       41 abort($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
813             join(', ', sort keys %unknown_keywords));
814             }
815              
816             # Note: we can remove all of this entirely and just rely on strict mode when we (eventually!) remove
817             # the traverse phase and replace with evaluate-against-metaschema.
818 33283 100 100     138371 if ($state->{seen_data_properties} and $is_object_data) {
819             # record the locations of all local properties
820             $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 0
821 156         1805 foreach grep !/^x-/, keys %$data;
822              
823             my @evaluated_properties = map {
824 156         552 my $keyword = $_->{keyword};
  577         1158  
825             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
826 577 100       2010 ? $_->{annotation}->@* : ();
827             } local_annotations($state);
828              
829             # tick off properties that were recognized by this subschema
830 156         641 $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 1 foreach @evaluated_properties;
831              
832             # weird! the draft4 metaschema doesn't know about '$ref' at all!
833             $state->{seen_data_properties}{$state->{data_path}.'/$ref'} |= 1
834 156 100 66     735 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
835             }
836              
837 33283 100 100     164388 if ($valid and $state->{collect_annotations} and $state->{specification_version} !~ /^draft(?:[467]|2019-09)\z/) {
      100        
838             annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
839 969         4229 foreach sort keys %unknown_keywords;
840             }
841              
842             # only keep new annotations if schema is valid
843 33283 100       138685 push $parent_annotations->@*, $state->{annotations}->@* if $valid;
844              
845             # only keep new defaults if schema is valid
846             $defaults->@{keys $state->{defaults}->%*} = values $state->{defaults}->%*
847 33283 100 100     131105 if $valid and $state->{defaults};
848              
849 33283         367011 return $valid;
850             }
851              
852             has _resource_index => (
853             is => 'bare',
854             isa => Map[my $resource_key_type = Str->where('!/#/'), my $resource_type = Dict[
855             canonical_uri => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
856             path => json_pointer_type, # JSON pointer relative to the document root
857             specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
858             document => InstanceOf['JSON::Schema::Modern::Document'],
859             # the vocabularies used when evaluating instance data against schema
860             vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
861             anchors => Optional[HashRef[Dict[
862             canonical_uri => canonical_uri_type, # equivalent uri with json pointer fragment
863             path => json_pointer_type, # JSON pointer relative to the document root
864             dynamic => Optional[Bool],
865             ]]],
866             Slurpy[HashRef[Undef]], # no other fields allowed
867             ]],
868             );
869              
870             sub _get_resource {
871 44006 50   44007   164807 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
872 44006   100     4248219 ($_[0]->{_resource_index}//{})->{$_[1]}
873             }
874              
875             # does not check for duplicate entries, or for malformed uris
876             sub _add_resources_unsafe {
877 47     47   103454 use autovivification 'store';
  47         146  
  47         325  
878             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
879 103     104   1376 foreach pairs @_[1..$#_];
880             }
881 25   50 26   84796 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
882 17381   100 17382   4220653 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
883 2121   50 2122   373296 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
884              
885 18963     18964   69389 sub _add_resource ($self, @kvs) {
  18963         39941  
  18963         51402  
  18963         35993  
886 18963         123748 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
887 18963         55032 my ($canonical_uri, $resource) = @$pair;
888              
889 18963 100       81701 if (my $existing = $self->_get_resource($canonical_uri)) {
    100          
890             # we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
891             # lack all identifiers altogether, but preserve other resources from the original document
892 17042 100       77587 if ($canonical_uri ne '') {
893             my @diffs = (
894             ($existing->{path} eq $resource->{path} ? () : 'path'),
895             ($existing->{canonical_uri} eq $resource->{canonical_uri} ? () : 'canonical_uri'),
896             ($existing->{specification_version} eq $resource->{specification_version} ? () : 'specification_version'),
897 859 100       9365 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
898 859 100       392644 next if not @diffs;
899 10         3604 croak 'uri "'.$canonical_uri.'" conflicts with an existing schema resource: documents differ by ',
900             join(', ', @diffs);
901             }
902             }
903             elsif (JSON::Schema::Modern::Utilities::get_schema_filename($canonical_uri)) {
904 2         708 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
905             }
906              
907 47     47   36443 use autovivification 'store';
  47         156  
  47         319  
908 18102         115845 $self->{_resource_index}{$resource_key_type->($canonical_uri)} = $resource_type->($resource);
909             }
910             }
911              
912             # $vocabulary uri (not its $id!) => [ specification_version, class ]
913             has _vocabulary_classes => (
914             is => 'bare',
915             isa => HashRef[
916             my $vocabulary_type = Tuple[
917             $spec_version_type,
918             $vocabulary_class_type,
919             ]
920             ],
921             reader => '__vocabulary_classes',
922             lazy => 1,
923             default => sub {
924             +{
925 12     13   1196 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   32  
  12     13   370  
  12     12   817  
  12     12   44  
  12     12   422  
  12     12   866  
  12     12   35  
  12     7   312  
  12     7   911  
  12     7   31  
  12     7   212  
  12     7   850  
  12     7   30  
  12     7   287  
  12     7   773  
  12     1   29  
  12     1   293  
  12     1   80  
  12     1   28  
  12     1   208  
  12     1   74  
  12         32  
  12         54  
  7         61  
  7         18  
  7         169  
  7         41  
  7         17  
  7         127  
  7         34  
  7         16  
  7         120  
  7         40  
  7         16  
  7         136  
  7         55  
  7         18  
  7         142  
  7         33  
  7         18  
  7         122  
  7         34  
  7         14  
  7         165  
  7         42  
  7         19  
  7         47  
  1         5  
  1         3  
  1         14  
  1         6  
  1         3  
  1         32  
  1         5  
  1         3  
  1         14  
  1         5  
  1         2  
  1         27  
  1         5  
  1         2  
  1         13  
  1         4  
  1         4  
  1         5  
926             map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
927             qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
928             }
929             },
930             );
931              
932 107     108   4657 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
933              
934 10     11 1 37880 sub add_vocabulary ($self, $classname) {
  10         24  
  10         24  
  10         19  
935 10 50       359 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
936              
937 10         3243 $vocabulary_class_type->(load_module($classname));
938              
939             # uri => version, uri => version
940 7         858 foreach my $pair (pairs $classname->vocabulary) {
941 7         162 my ($uri_string, $spec_version) = @$pair;
942 7         66 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
943 6         11271 $spec_version_type->($spec_version);
944              
945 4 100       687 croak 'keywords starting with "$" are reserved for core and cannot be used'
946             if grep /^\$/, $classname->keywords;
947              
948 3         36 $self->{_vocabulary_classes}{$uri_string} = $vocabulary_type->([ $spec_version, $classname ]);
949             }
950             }
951              
952             # $schema uri => [ specification_version, [ vocab classes, in evaluation order ] ].
953             has _metaschema_vocabulary_classes => (
954             is => 'bare',
955             isa => HashRef[
956             my $mvc_type = Tuple[
957             $spec_version_type,
958             ArrayRef[$vocabulary_class_type],
959             ]
960             ],
961             reader => '__metaschema_vocabulary_classes',
962             lazy => 1,
963             default => sub {
964 38     38   25968 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   189  
  38     38   1517  
  38     38   22532  
  38     38   264  
  38     38   1722  
  38     38   25885  
  38     30   231  
  38     30   1656  
  38     30   25254  
  38     30   237  
  38     30   1569  
  38     30   23985  
  38     30   187  
  38     19   1617  
  38     19   30131  
  38     19   437  
  38     19   1463  
  38     19   328  
  38     19   81  
  38     19   238  
  30     16   265  
  30     15   84  
  30     15   631  
  30     15   170  
  30     15   67  
  30     15   653  
  30     15   177  
  30     13   80  
  30     13   508  
  30     13   134  
  30     13   95  
  30     13   681  
  30     13   260  
  30     13   63  
  30     12   813  
  30     12   152  
  30     12   67  
  30     12   556  
  30     12   143  
  30     12   60  
  30     12   171  
  19     11   240  
  19     11   64  
  19     11   479  
  19     11   131  
  19     11   38  
  19     11   325  
  19     11   102  
  19     10   43  
  19     10   447  
  19     10   119  
  19     9   37  
  19     9   405  
  19     9   98  
  19     9   38  
  19     9   374  
  19     9   89  
  19     9   43  
  19     9   359  
  19     9   86  
  19     9   37  
  19     9   130  
  16     9   124  
  16     9   38  
  16     9   423  
  15     9   76  
  15     9   33  
  15     9   367  
  15     9   80  
  15     8   35  
  15     8   250  
  15     8   67  
  15     8   28  
  15     8   260  
  15     8   71  
  15     8   28  
  15     6   267  
  15     6   79  
  15     6   31  
  15     6   240  
  15     6   64  
  15     6   27  
  15     6   90  
  13     6   102  
  13     6   34  
  13     6   259  
  13     6   59  
  13     6   41  
  13     6   189  
  13     6   64  
  13     6   47  
  13     6   231  
  13     6   62  
  13     5   30  
  13     5   238  
  13     5   59  
  13     5   30  
  13     5   224  
  13     5   56  
  13     5   24  
  13     5   217  
  13     5   57  
  13     5   27  
  13     5   110  
  12     6   106  
  12     6   59  
  12     6   241  
  12     6   62  
  12     6   29  
  12     6   214  
  12     6   62  
  12     6   25  
  12     4   280  
  12     4   63  
  12     4   24  
  12     4   227  
  12     4   58  
  12     4   26  
  12     4   241  
  12     4   76  
  12     4   28  
  12     4   225  
  12     4   58  
  12     4   24  
  12     4   82  
  11         79  
  11         23  
  11         238  
  11         53  
  11         29  
  11         189  
  11         54  
  11         26  
  11         173  
  11         45  
  11         22  
  11         150  
  11         42  
  11         17  
  11         216  
  11         46  
  11         18  
  11         197  
  11         44  
  11         25  
  11         58  
  10         80  
  10         23  
  10         215  
  10         39  
  10         21  
  10         154  
  9         99  
  9         18  
  9         146  
  9         38  
  9         18  
  9         160  
  9         36  
  9         14  
  9         140  
  9         32  
  9         16  
  9         110  
  9         30  
  9         19  
  9         62  
  9         74  
  9         17  
  9         169  
  9         41  
  9         21  
  9         147  
  9         50  
  9         18  
  9         142  
  9         40  
  9         18  
  9         179  
  9         42  
  9         21  
  9         182  
  9         50  
  9         20  
  9         144  
  9         71  
  9         17  
  9         59  
  9         76  
  9         26  
  9         255  
  9         45  
  9         21  
  9         584  
  9         52  
  9         39  
  9         161  
  9         43  
  9         25  
  9         142  
  9         38  
  9         19  
  9         198  
  9         63  
  9         20  
  9         225  
  9         58  
  9         23  
  9         58  
  8         71  
  8         21  
  8         177  
  8         43  
  8         22  
  8         131  
  8         49  
  8         19  
  8         1381  
  8         57  
  8         25  
  8         189  
  8         44  
  8         18  
  8         152  
  8         40  
  8         19  
  8         133  
  8         41  
  8         30  
  8         75  
  6         49  
  6         17  
  6         104  
  6         29  
  6         17  
  6         125  
  6         38  
  6         14  
  6         121  
  6         28  
  6         13  
  6         88  
  6         28  
  6         12  
  6         157  
  6         40  
  6         13  
  6         159  
  6         44  
  6         12  
  6         45  
  6         50  
  6         14  
  6         121  
  6         27  
  6         16  
  6         112  
  6         38  
  6         12  
  6         109  
  6         30  
  6         11  
  6         97  
  6         30  
  6         12  
  6         102  
  6         28  
  6         13  
  6         147  
  6         33  
  6         13  
  6         60  
  6         710  
  6         4896  
  6         149  
  6         822  
  6         4644  
  6         127  
  6         690  
  6         4556  
  6         104  
  5         22  
  5         9  
  5         64  
  5         22  
  5         10  
  5         118  
  5         25  
  5         10  
  5         84  
  5         21  
  5         14  
  5         31  
  5         41  
  5         11  
  5         126  
  5         26  
  5         12  
  5         59  
  5         27  
  5         12  
  5         68  
  5         28  
  5         9  
  5         62  
  5         22  
  5         10  
  5         127  
  5         26  
  5         11  
  5         62  
  5         19  
  5         9  
  5         33  
  6         50  
  6         13  
  6         164  
  6         32  
  6         12  
  6         101  
  6         35  
  6         12  
  6         132  
  6         33  
  6         28  
  6         101  
  6         24  
  6         11  
  6         89  
  6         25  
  6         15  
  6         111  
  6         27  
  6         13  
  6         51  
  6         38  
  6         11  
  6         108  
  4         20  
  4         9  
  4         52  
  4         23  
  4         9  
  4         73  
  4         20  
  4         11  
  4         100  
  4         20  
  4         9  
  4         52  
  4         15  
  4         8  
  4         62  
  4         17  
  4         8  
  4         30  
  4         30  
  4         10  
  4         68  
  4         20  
  4         10  
  4         46  
  4         23  
  4         9  
  4         82  
  4         23  
  4         8  
  4         80  
  4         17  
  4         9  
  4         55  
  4         16  
  4         7  
  4         98  
  4         19  
  4         8  
  4         29  
965             qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated);
966             +{
967             'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
968             do { pop @modules; () }, # remove Unevaluated
969             'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', [ @modules ] ],
970             'http://json-schema.org/draft-07/schema' => [ 'draft7', [ @modules ] ],
971             do { splice @modules, 4, 1; () }, # remove Content
972             'http://json-schema.org/draft-06/schema' => [ 'draft6', \@modules ],
973             'http://json-schema.org/draft-04/schema' => [ 'draft4', \@modules ],
974             },
975             },
976             );
977              
978 30489     30490   986212 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
979 5924     5925   48796 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
980 4     5   322 sub __all_metaschema_vocabulary_classes { values $_[0]->__metaschema_vocabulary_classes->%* }
981              
982             # translate vocabulary URIs into classes, caching the results (if any)
983 53     54   161 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         142  
  53         190  
  53         120  
  53         193  
984 53 100       462 if (not exists $schema_info->{schema}{'$vocabulary'}) {
985             # "If "$vocabulary" is absent, an implementation MAY determine behavior based on the meta-schema
986             # if it is recognized from the URI value of the referring schema's "$schema" keyword."
987 2         18 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
988 2         8 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
989             }
990              
991 51         142 my $valid = 1;
992             # Core §8.1.2-6: "The "$vocabulary" keyword SHOULD be used in the root schema of any schema
993             # document intended for use as a meta-schema. It MUST NOT appear in subschemas."
994 51 100       261 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
995 51 100       422 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
996              
997 51 100       214 return (undef, []) if not $valid;
998              
999 49         121 my @vocabulary_classes;
1000              
1001 49         544 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
1002 105         415 my $class_info = $self->_get_vocabulary_class($uri);
1003             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" is not a known vocabulary', $uri), next
1004 105 100 100     17783 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1005              
1006 97 100       1414 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1007              
1008 89         279 my ($spec_version, $class) = @$class_info;
1009             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" uses %s, but the metaschema itself uses %s',
1010             $uri, $spec_version, $schema_info->{specification_version}), next
1011 89 100       492 if $spec_version ne $schema_info->{specification_version};
1012              
1013 83         293 push @vocabulary_classes, $class;
1014             }
1015              
1016             @vocabulary_classes = sort {
1017 49 50       298 $a->evaluation_order <=> $b->evaluation_order
  49 50       303  
1018             || ($a->evaluation_order == 999 ? 0
1019             : ($valid = E($state, '%s and %s have a conflicting evaluation_order', sort $a, $b)))
1020             } @vocabulary_classes;
1021              
1022 49 100 100     360 $valid = E($state, 'the first vocabulary (by evaluation_order) must be Core')
1023             if ($vocabulary_classes[0]//'') ne 'JSON::Schema::Modern::Vocabulary::Core';
1024              
1025 49         134 my %seen_keyword;
1026 49         168 foreach my $class (@vocabulary_classes) {
1027 83         701 foreach my $keyword ($class->keywords($schema_info->{specification_version})) {
1028             $valid = E($state, '%s keyword "%s" conflicts with keyword of the same name from %s',
1029             $class, $keyword, $seen_keyword{$keyword})
1030 794 100       1727 if $seen_keyword{$keyword};
1031 794         2133 $seen_keyword{$keyword} = $class;
1032             }
1033             }
1034              
1035 49 100       932 return ($schema_info->{specification_version}, $valid ? \@vocabulary_classes : []);
1036             }
1037              
1038             # used for determining a default '$schema' keyword where there is none
1039             # these are also normalized as this is how we cache them
1040 47         8694 use constant METASCHEMA_URIS => {
1041             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
1042             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
1043             'draft7' => 'http://json-schema.org/draft-07/schema',
1044             'draft6' => 'http://json-schema.org/draft-06/schema',
1045             'draft4' => 'http://json-schema.org/draft-04/schema',
1046 47     47   97693 };
  47         134  
1047              
1048             # for internal use only. files are under share/
1049 47         75417 use constant _CACHED_METASCHEMAS => {
1050             'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
1051             'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
1052             'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
1053             'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
1054             'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
1055             'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
1056             'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
1057             'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
1058             'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
1059             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
1060              
1061             'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
1062             'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
1063             'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
1064             'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
1065             'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
1066             'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
1067             'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
1068             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
1069              
1070             # trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
1071             'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
1072             'http://json-schema.org/draft-06/schema' => 'draft6/schema.json',
1073             'http://json-schema.org/draft-04/schema' => 'draft4/schema.json',
1074 47     47   422 };
  47         126  
1075              
1076             # simple runtime-wide cache of metaschema document objects that are sourced from disk
1077             my $metaschema_cache = {};
1078              
1079             {
1080             my $share_dir = dist_dir('JSON-Schema-Modern');
1081             JSON::Schema::Modern::Utilities::register_schema($_, $share_dir.'/'._CACHED_METASCHEMAS->{$_})
1082             foreach keys _CACHED_METASCHEMAS->%*;
1083             }
1084              
1085             # returns the same as _get_resource
1086 24412     24413   1453315 sub _get_or_load_resource ($self, $uri) {
  24412         53415  
  24412         51536  
  24412         42934  
1087 24412         89726 my $resource = $self->_get_resource($uri);
1088 24412 100       3439445 return $resource if $resource;
1089              
1090 114 100       681 if (my $document = load_cached_document($self, $uri)) {
1091 103         533 return $self->_get_resource($uri);
1092             }
1093              
1094             # TODO:
1095             # - load from network or disk
1096              
1097 11         33 return;
1098             };
1099              
1100             # returns information necessary to use a schema found at a particular URI or uri-reference:
1101             # - schema: a schema (which may not be at a document root)
1102             # - canonical_uri: the canonical uri for that schema,
1103             # - document: the JSON::Schema::Modern::Document object that holds that schema
1104             # - document_path: the path relative to the document root for this schema
1105             # - specification_version: the specification version that applies to this schema
1106             # - vocabularies: the vocabularies to use when considering schema keywords
1107             # creates a Document and adds it to the resource index, if not already present.
1108 24174     24175   402018 sub _fetch_from_uri ($self, $uri_reference) {
  24174         50567  
  24174         64478  
  24174         43253  
1109 24174 50       128515 $uri_reference = Mojo::URL->new($uri_reference) if not is_schema($uri_reference);
1110              
1111             # this is *a* resource that would contain our desired location, but may not be the closest one
1112 24174         8882732 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1113 24174 100       184609 return if not $resource;
1114              
1115 24163         74643 my $fragment = $uri_reference->fragment;
1116 24163 100 100     191340 if (not length($fragment) or $fragment =~ m{^/}) {
1117 23583   100     294457 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1118 23583 100       270224 return if not defined $subschema;
1119              
1120 23581         49298 my $closest_resource;
1121 23581 100       65510 if (not length $fragment) { # we already have the canonical resource root
1122 21460         64309 $closest_resource = [ undef, $resource ];
1123             }
1124             else {
1125             # determine the canonical uri by finding the closest schema resource(s)
1126 2121         8668 my $doc_addr = refaddr($resource->{document});
1127             my @closest_resources =
1128 500         4316 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1129             grep { !length($_->[1]{path}) # document root
1130 2909 100 66     36205 || length($document_path)
1131             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1132 2121         10456 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228326         536366  
1133             $self->_resource_pairs;
1134              
1135             # now whittle down to all the resources with the same document path as the first candidate
1136 2121 100       62981 if (@closest_resources > 1) {
1137             # find the resource key that most closely matches the original query uri, by matching prefixes
1138 462         2932 my $match = $uri_reference.'';
1139             @closest_resources =
1140 24         117 sort { _prefix_match_length($b->[0], $match) <=> _prefix_match_length($a->[0], $match) }
1141             grep $_->[1]{path} eq $closest_resources[0]->[1]{path},
1142 462         139289 @closest_resources;
1143             }
1144              
1145 2121         6353 $closest_resource = $closest_resources[0];
1146             }
1147              
1148             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1149 23581         150179 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1150 23581 100       2430292 $canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
1151              
1152             return {
1153             schema => $subschema,
1154             canonical_uri => $canonical_uri,
1155             document_path => $document_path,
1156 23581         560553 $closest_resource->[1]->%{qw(document specification_version vocabularies)}, # reference, not copy
1157             };
1158             }
1159             else { # we are following a URI with a plain-name fragment
1160 580 100 100     5799 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
1161             return {
1162             schema => $resource->{document}->get($subresource->{path}),
1163             canonical_uri => $subresource->{canonical_uri}, # this is *not* the anchor-containing URI
1164             document_path => $subresource->{path},
1165 578         4460 $resource->%{qw(document specification_version vocabularies)}, # reference, not copy
1166             };
1167             }
1168             }
1169              
1170             # given two strings, determines the number of characters in common, starting from the first
1171             # character
1172 48     49   81 sub _prefix_match_length ($x, $y) {
  48         77  
  48         76  
  48         67  
1173 48         151 my $len = min(length($x), length($y));
1174 48         135 foreach my $pos (0..$len) {
1175 1406 100       2562 return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
1176             }
1177 0         0 return $len;
1178             }
1179              
1180             # Mojo::JSON::JSON_XS is false when the environment variable $MOJO_NO_JSON_XS is set
1181             # and also checks if Cpanel::JSON::XS is installed.
1182             # Mojo::JSON falls back to its own pure-perl encoder/decoder but does not support all the options
1183             # that we require here.
1184             use constant _JSON_BACKEND =>
1185 47         970 Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION('4.38'); 1 } ? 'Cpanel::JSON::XS'
  47         94182  
1186 47 0       121 : eval { JSON::PP->VERSION('4.11'); 1 } ? 'JSON::PP'
  2 50       4  
  2         30  
1187 47     47   705 : die 'Cpanel::JSON::XS 4.38 or JSON::PP 4.11 is required';
  47         125  
1188              
1189             # used for internal encoding as well (when caching serialized schemas)
1190             has _json_decoder => (
1191             is => 'ro',
1192             isa => HasMethods[qw(encode decode)],
1193             lazy => 1,
1194             default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
1195             );
1196              
1197             # since media types are case-insensitive, all type names must be casefolded on insertion.
1198             has _media_type => (
1199             is => 'bare',
1200             isa => my $media_type_type = Map[Str->where(q{$_ eq CORE::fc($_)}), CodeRef],
1201             reader => '__media_type',
1202             lazy => 1,
1203             default => sub ($self) {
1204             my $_json_media_type = sub ($content_ref) {
1205             # utf-8 decoding is always done, as per the JSON spec.
1206             # other charsets are not supported: see RFC8259 §11
1207             \ _JSON_BACKEND->new->allow_nonref(1)->utf8(1)->decode($content_ref->$*);
1208             };
1209             +{
1210             (map +($_ => $_json_media_type),
1211             qw(application/json application/schema+json application/schema-instance+json)),
1212             (map +($_ => sub ($content_ref) { $content_ref }),
1213             qw(text/* application/octet-stream)),
1214             'application/x-www-form-urlencoded' => sub ($content_ref) {
1215             \ Mojo::Parameters->new->charset('UTF-8')->parse($content_ref->$*)->to_hash;
1216             },
1217             'application/x-ndjson' => sub ($content_ref) {
1218             my $decoder = _JSON_BACKEND->new->allow_nonref(1)->utf8(1);
1219             my $line = 0; # line numbers start at 1
1220             \[ map {
1221             do {
1222             try { ++$line; $decoder->decode($_) }
1223             catch ($e) { die 'parse error at line '.$line.': '.$e }
1224             }
1225             }
1226             split(/\r?\n/, $content_ref->$*)
1227             ];
1228             },
1229             };
1230             },
1231             );
1232              
1233 5     6 1 2912 sub add_media_type { $media_type_type->({ @_[1..2] }); $_[0]->__media_type->{$_[1]} = $_[2]; }
  4         297  
1234              
1235             # get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
1236 38     39 1 11106 sub get_media_type ($self, $type) {
  38         96  
  38         116  
  38         65  
1237 38         1154 my $types = $self->__media_type;
1238 38         1058 my $mt = $types->{fc $type};
1239 38 100       194 return $mt if $mt;
1240              
1241 9 100 100 42   69 return $types->{(first { m{([^/]+)/\*\z} && fc($type) =~ m{^\Q$1\E/[^/]+\z} } keys %$types) // '*/*'};
  41         372  
1242             };
1243              
1244             has _encoding => (
1245             is => 'bare',
1246             isa => HashRef[CodeRef],
1247             reader => '__encoding',
1248             lazy => 1,
1249             default => sub ($self) {
1250             +{
1251             identity => sub ($content_ref) { $content_ref },
1252             base64 => sub ($content_ref) {
1253             die "invalid characters\n"
1254             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
1255             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
1256             },
1257             base64url => sub ($content_ref) {
1258             die "invalid characters\n"
1259             if $content_ref->$* =~ m{[^A-Za-z0-9=_-]} or $content_ref->$* =~ m{=(?=[^=])};
1260             require MIME::Base64; \ MIME::Base64::decode_base64url($content_ref->$*);
1261             },
1262             };
1263             },
1264             );
1265              
1266 22     23 1 938 sub get_encoding { $_[0]->__encoding->{$_[1]} }
1267 0     1 1 0 sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
1268              
1269             # callback hook for Sereal::Encoder
1270 3     4 0 1853 sub FREEZE ($self, $serializer) {
  3         7  
  3         8  
  3         4  
1271 3         49 my $data = +{ %$self };
1272             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1273             # coderefs can't serialize cleanly and must be re-added by the user.
1274 3         21 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1275 3         109 return $data;
1276             }
1277              
1278             # callback hook for Sereal::Decoder
1279 4     5 0 1397 sub THAW ($class, $serializer, $data) {
  4         9  
  4         11  
  4         8  
  4         7  
1280 4         11 my $self = bless($data, $class);
1281              
1282             # load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
1283             load_module($_)
1284 4         26 foreach uniq(
1285             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1286             (map $_->[1], values $self->__vocabulary_classes->%*));
1287              
1288 4         124 return $self;
1289             }
1290              
1291             1;
1292              
1293             __END__