File Coverage

blib/lib/JSON/Schema/Modern.pm
Criterion Covered Total %
statement 1013 1027 98.6
branch 263 320 82.1
condition 161 186 86.5
subroutine 229 229 100.0
pod 14 16 87.5
total 1680 1778 94.4


line stmt bran cond sub pod time code
1 50     50   18989542 use strict;
  50         121  
  50         2234  
2 50     50   304 use warnings;
  50         115  
  50         5297  
3             package JSON::Schema::Modern; # git description: v0.633-4-g3475aaa5
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.634';
9              
10 50     50   1042 use 5.020; # for fc, unicode_strings features
  50         224  
11 50     50   31061 use Moo;
  50         426116  
  50         293  
12 50     50   84523 use strictures 2;
  50         478  
  50         2186  
13 50     50   24252 use stable 0.031 'postderef';
  50         1029  
  50         413  
14 50     50   10672 use experimental 'signatures';
  50         206  
  50         257  
15 50     50   3307 no autovivification warn => qw(fetch store exists delete);
  50         220  
  50         441  
16 50     49   4883 use if "$]" >= 5.022, experimental => 're_strict';
  49         147  
  49         1694  
17 49     49   4717 no if "$]" >= 5.031009, feature => 'indirect';
  49         137  
  49         3936  
18 49     49   333 no if "$]" >= 5.033001, feature => 'multidimensional';
  49         137  
  49         3373  
19 49     49   353 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  49         191  
  49         3179  
20 49     49   347 no if "$]" >= 5.041009, feature => 'smartmatch';
  49         137  
  49         2535  
21 49     49   444 no feature 'switch';
  49         259  
  49         1829  
22 49     49   28965 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  49         10943399  
  49         2770  
23 49     49   574 use Carp qw(croak carp);
  49         353  
  49         5017  
24 49     48   391 use List::Util 1.55 qw(pairs first uniqint pairmap uniq min);
  48         1765  
  47         6120  
25 47     48   374 use if "$]" < 5.041010, 'List::Util' => 'any';
  48         778  
  48         2799  
26 48     48   302 use if "$]" >= 5.041010, experimental => 'keyword_any';
  48         685  
  47         1235  
27 47     48   32543 use builtin::compat qw(refaddr load_module);
  48         928907  
  48         4717  
28 48     48   35384 use Mojo::URL;
  48         529163  
  48         5275  
29 48     48   31426 use Safe::Isa;
  48         33507  
  48         12210  
30 48     48   25527 use Mojo::File 'path';
  48         1029070  
  48         8841  
31 48     47   530 use Storable 'dclone';
  47         110  
  47         4513  
32 47     47   350 use File::ShareDir 'dist_dir';
  47         107  
  47         3747  
33 47     47   27617 use MooX::TypeTiny 0.002002;
  47         21136  
  47         312  
34 47     47   433590 use Types::Standard 1.016003 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict CodeRef Optional Slurpy ArrayRef Undef ClassName Tuple Map);
  47         6599706  
  47         762  
35 47     47   263383 use Digest::MD5 'md5';
  47         166  
  47         16113  
36 47     47   30983 use Feature::Compat::Try;
  47         20268  
  47         505  
37 47     47   31524 use JSON::Schema::Modern::Error;
  47         1160  
  47         2410  
38 47     47   31108 use JSON::Schema::Modern::Result;
  47         790  
  47         2834  
39 47     47   33055 use JSON::Schema::Modern::Document;
  47         1227  
  47         1061  
40 47     47   4295 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);
  47         135  
  47         7012  
41 47     47   421 use namespace::clean;
  47         138  
  47         559  
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   42984 use constant SPECIFICATION_VERSION_DEFAULT => 'draft2020-12';
  47         136  
  47         4694  
52 47     47   410 use constant SPECIFICATION_VERSIONS_SUPPORTED => [qw(draft4 draft6 draft7 draft2019-09 draft2020-12)];
  47         133  
  47         46233  
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   1917 sub _get_format_validation ($self, $format) { ($self->{_format_validations}//{})->{$format} }
  969         1610  
  969         1750  
  969         1546  
  969         12085  
117              
118 12     12 1 43514 sub add_format_validation ($self, $format, $definition) {
  12         34  
  12         23  
  12         41  
  12         25  
119 12 50 100     94 return if exists(($self->{_format_validations}//{})->{$format});
120              
121 12 100       75 $definition = { type => 'string', sub => $definition } if ref $definition ne 'HASH';
122 12         115 $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     368 if core_formats_type->check($format) and $definition->{type} ne 'string';
127              
128 47     47   517 use autovivification 'store';
  47         135  
  47         540  
129 4         3623 $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 17393 50   17393 1 228015 croak 'insufficient arguments' if @_ < 2;
151 17393         35888 my $self = shift;
152              
153 17393 100       89363 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
154 2         340 Carp::carp('use of deprecated form of add_schema with document');
155 2         17 return $self->add_document($_[0]);
156             }
157              
158             # TODO: resolve $uri against $self->base_uri
159 17392 50       249386 my $uri = !is_schema($_[0]) ? Mojo::URL->new(shift)
    100          
160             : $_[0]->$_isa('Mojo::URL') ? shift : Mojo::URL->new;
161              
162 17392 100       336151 croak 'cannot add a schema with a uri with a fragment' if defined $uri->fragment;
163 17391 50       126001 croak 'insufficient arguments' if not @_;
164              
165 17391 100       54219 if ($_[0]->$_isa('JSON::Schema::Modern::Document')) {
166 2         454 Carp::carp('use of deprecated form of add_schema with document');
167 2         18 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 17390         694417 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 17390         464316 my $schema_checksum = $document->_checksum(md5($self->_json_decoder->encode($document->schema)));
181 17390 100       1335121 if (my $existing_doc = first {
182 778548   66 778548   13551179 my $existing_checksum = $_->_checksum
183             // $_->_checksum(md5($self->_json_decoder->encode($_->schema)));
184 778548 100       5435934 $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         3873488 $document = $existing_doc;
189             }
190              
191 17390         696945 $self->add_document($uri, $document);
192             }
193              
194             sub add_document {
195 18202 50   18202 1 152171 croak 'insufficient arguments' if @_ < 2;
196 18202         47028 my $self = shift;
197              
198             # TODO: resolve $uri against $self->base_uri
199 18202 50       91498 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 18202 50       7347755 croak 'cannot add a schema with a uri with a fragment' if defined $base_uri->fragment;
203 18202 50       139561 croak 'insufficient arguments' if not @_;
204              
205 18202         40458 my $document = shift;
206 18202 50       67094 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 18202 100       323233 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 18022 100       77369 if (not length $base_uri){
217 17189         2420409 foreach my $res_pair ($document->resource_pairs) {
218 18048         312610 my ($uri_string, $doc_resource) = @$res_pair;
219              
220             # this might croak if there are duplicates or malformed entries.
221 18048         193182 $self->_add_resource($uri_string => +{ $doc_resource->%*, document => $document });
222             }
223              
224 17184         9011545 return $document;
225             }
226              
227 834         198287 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         4063 foreach my $res_pair ($document->resource_pairs) {
231 864         2808 my ($uri_string, $doc_resource) = @$res_pair;
232 864         3231 $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         603259 $doc_resource->%{qw(path specification_version vocabularies)},
237             document => $document,
238             };
239              
240 864   100     498400 foreach my $anchor (keys (($doc_resource->{anchors}//{})->%*)) {
241 47     47   83460 use autovivification 'store';
  47         158  
  47         284  
242             $new_resource->{anchors}{$anchor} = {
243             $doc_resource->{anchors}{$anchor}->%{path},
244             (map +($_->[1] ? @$_ : ()), [ $doc_resource->{anchors}{$anchor}->%{dynamic} ]),
245 170 100       2339 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         94423 $self->_add_resource($uri_string => $new_resource);
251 857 100 66     494163 @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       5076 $self->_add_resource($base_uri.'' => $root[1]) if $root[0] ne $base_uri;
256              
257 827         208342 return $document;
258             }
259              
260 4     4 1 13744 sub evaluate_json_string ($self, $json_data, $schema, $config_override = {}) {
  4         12  
  4         26  
  4         12  
  4         10  
  4         23  
261 4 50       18 croak 'evaluate_json_string called in void context' if not defined wantarray;
262              
263 4         165 my $data;
264 4         48 try {
265 4         121 $data = $self->_json_decoder->decode($json_data)
266             }
267             catch ($e) {
268 3         124 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         61 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 17942     17942 1 112830 sub traverse ($self, $schema_reference, $config_override = {}) {
  17942         35916  
  17942         36591  
  17942         40885  
  17942         36258  
293 17942         74007 my %overrides = %$config_override;
294 17942         88698 delete @overrides{qw(callbacks initial_schema_uri metaschema_uri traversed_keyword_path specification_version skip_ref_checks)};
295 17942 50       61411 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 17942   66     134870 my $initial_uri = Mojo::URL->new($config_override->{initial_schema_uri} // ());
301 17942   100     7595655 my $initial_path = $config_override->{traversed_keyword_path} // '';
302 17942   100     196904 my $spec_version = $config_override->{specification_version} // $self->specification_version // SPECIFICATION_VERSION_DEFAULT;
      100        
303              
304 17942 50       111140 croak 'traversed_keyword_path must be a json pointer' if $initial_path !~ m{^(?:/|\z)};
305              
306 17942 100       65706 if (length(my $uri_path = $initial_uri->fragment)) {
307 5 50       33 croak 'initial_schema_uri fragment must be a json pointer' if $uri_path !~ m{^/};
308              
309 5 50       35 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 17942 100 100     438158 evaluator => $self,
326             traverse => 1,
327             };
328              
329 17942         52782 my $valid = 1;
330              
331 17942         46940 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 17942 100 100     306591 : $config_override->{metaschema_uri}) // $self->METASCHEMA_URIS->{$spec_version};
      66        
339              
340 17942 100       101669 if (my $metaschema_info = $self->_get_metaschema_vocabulary_classes($state->{metaschema_uri})) {
341 17936         671565 $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       2254 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         54 ->_traverse_keyword_schema({ '$schema' => $state->{metaschema_uri}.'' }, $state);
352             }
353              
354 17942 100 66     203153 $valid = $self->_traverse_subschema($schema_reference, $state) if $valid and not $state->{errors}->@*;
355 17942 50 66     61447 die 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
356 17942 50 66     178156 die 'result is true but there are errors' if $valid and $state->{errors}->@*;
357             }
358             catch ($e) {
359 1 0       31 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    0          
360 1         7 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         4 push $state->{errors}->@*, $e;
365             }
366             else {
367 1         20 E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
368             }
369             }
370              
371 17942         109008 return $state;
372             }
373              
374             # the actual runtime evaluation of the schema against input data.
375 17388     17388 1 33981819 sub evaluate ($self, $data, $schema_reference, $config_override = {}) {
  17388         43742  
  17388         40002  
  17388         35616  
  17388         47857  
  17388         36293  
376 17388 50       72781 croak 'evaluate called in void context' if not defined wantarray;
377              
378 17388         62729 my %overrides = %$config_override;
379 17388         84703 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 17388 50       57264 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 17388   100     261077 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 17387         322318 my $valid;
393 17387         42456 try {
394 17387 100 100     89601 if (is_schema($schema_reference)) {
    100          
395             # traverse is called via add_schema -> ::Document->new -> ::Document->BUILD
396 17313         78377 $schema_reference = $self->add_schema($schema_reference)->canonical_uri;
397             }
398             elsif (ref $schema_reference and not $schema_reference->$_isa('Mojo::URL')) {
399 4         71 abort($state, 'invalid schema type: %s', get_type($schema_reference));
400             }
401              
402 17198         236907 my $schema_info = $self->_fetch_from_uri($schema_reference);
403 17198 100       64472 abort($state, 'EXCEPTION: unable to find resource "%s"', $schema_reference)
404             if not $schema_info;
405              
406             abort($state, 'EXCEPTION: "%s" is not a schema', $schema_reference)
407 17192 100       119505 if not $schema_info->{document}->get_entity_at_location($schema_info->{document_path});
408              
409             $state = +{
410             %$state,
411             initial_schema_uri => $schema_info->{canonical_uri}, # the canonical URI as of the start of evaluation, or last $id or $ref
412             $schema_info->%{qw(document specification_version vocabularies)},
413             dynamic_scope => [ $schema_info->{canonical_uri}->clone->fragment(undef) ],
414             annotations => [],
415             seen => {},
416             callbacks => $config_override->{callbacks} // {},
417             evaluator => $self,
418             (map {
419 120337   100     3283805 my $val = $config_override->{$_} // $self->$_;
420 120337 100       1160259 defined $val ? ($_ => $val) : ()
421             # note: this is a subset of the allowed overrides defined above
422             } qw(validate_formats validate_content_schemas short_circuit collect_annotations scalarref_booleans stringy_numbers strict)),
423 17191 100 100     206689 $config_override->{with_defaults} // $self->with_defaults ? (defaults => {}) : (),
      66        
424             };
425              
426             # this hash will be added to at each level of schema evaluation
427 17191 100       105318 $state->{seen_data_properties} = {} if $config_override->{_strict_schema_data};
428              
429             # we're going to set collect_annotations during evaluation when we see an unevaluated* keyword
430             # (or for object data when the _strict_schema_data configuration is set),
431             # but after we pass to a new data scope we'll clear it again.. unless we've got the config set
432             # globally for the entire evaluation, so we store that value in a high bit.
433 17191   100     106232 $state->{collect_annotations} = ($state->{collect_annotations}//0) << 8;
434              
435 17191         116111 $valid = $self->_eval_subschema($data, $schema_info->{schema}, $state);
436 17168 50 66     94423 warn 'result is false but there are no errors' if not $valid and not $state->{errors}->@*;
437 17168 50 66     169525 warn 'result is true but there are errors' if $valid and $state->{errors}->@*;
438             }
439             catch ($e) {
440 219 100       1647 if ($e->$_isa('JSON::Schema::Modern::Result')) {
    100          
441 180         4298 return $e;
442             }
443             elsif ($e->$_isa('JSON::Schema::Modern::Error')) {
444 34         1069 push $state->{errors}->@*, $e;
445             }
446             else {
447 5         190 $valid = E({ %$state, exception => 1 }, 'EXCEPTION: '.$e);
448             }
449             }
450              
451 17207 100       76343 if ($state->{seen_data_properties}) {
452 5         11 my %unknown_keywords;
453 5         69 foreach my $property (sort grep !$state->{seen_data_properties}{$_},
454             keys $state->{seen_data_properties}->%*) {
455 15         71 my ($parent, $keyword) = ($property =~ m{^(.*)/([^/]*)\z});
456 15   100     61 push(($unknown_keywords{$parent}//=[])->@*, $keyword);
457             }
458              
459 5         25 foreach my $parent (sort keys %unknown_keywords) {
460             $valid = E({ %$state, data_path => $parent },
461             'unknown keyword%s seen in schema: %s', $unknown_keywords{$parent}->@* > 1 ? 's' : '',
462 8 100       136 join(', ', sort $unknown_keywords{$parent}->@*));
463             }
464             }
465              
466 17207 50 50     115034 die 'evaluate validity inconsistent with error count' if $valid xor !$state->{errors}->@*;
467              
468             return JSON::Schema::Modern::Result->new(
469             output_format => $self->output_format,
470             valid => $valid,
471             $valid
472             # strip annotations from result if user didn't explicitly ask for them
473             ? ($config_override->{collect_annotations} // $self->collect_annotations
474             ? (annotations => $state->{annotations}) : ())
475             : (errors => $state->{errors}),
476 17207 100 100     782896 $state->{defaults} ? (defaults => $state->{defaults}) : (),
    100          
    100          
477             );
478             }
479              
480 10     11 1 50193 sub validate_schema ($self, $schema, $config_override = {}) {
  10         48  
  10         22  
  10         27  
  10         27  
481 10 50       46 croak 'validate_schema called in void context' if not defined wantarray;
482              
483             my $metaschema_uri = ref $schema eq 'HASH' && $schema->{'$schema'} ? $schema->{'$schema'}
484 10 100 66     159 : $self->METASCHEMA_URIS->{$self->specification_version // $self->SPECIFICATION_VERSION_DEFAULT};
      33        
485              
486             my $result = $self->evaluate($schema, $metaschema_uri,
487 10 100 100     274 { %$config_override, $self->strict || $config_override->{strict} ? (_strict_schema_data => 1) : () });
488              
489 10 100       328 return $result if not $result->valid;
490              
491             # the traversal pass will validate all constraints that weren't handled by the metaschema
492 3         99 my $state = $self->traverse($schema);
493             return JSON::Schema::Modern::Result->new(
494             output_format => $self->output_format,
495             valid => 0,
496             errors => $state->{errors},
497 3 100       63 ) if $state->{errors}->@*;
498              
499 2         51 return $result; # valid: true
500             }
501              
502 8     9 1 45727 sub get ($self, $uri_reference) {
  8         20  
  8         21  
  8         16  
503 8 100       33 if (wantarray) {
504 5         29 my $schema_info = $self->_fetch_from_uri($uri_reference);
505 5 100       33 return if not $schema_info;
506 4 100       706 my $subschema = ref $schema_info->{schema} ? dclone($schema_info->{schema}) : $schema_info->{schema};
507 4         59 return ($subschema, $schema_info->{canonical_uri});
508             }
509             else { # abridged version of _fetch_from_uri
510 3 50       28 $uri_reference = Mojo::URL->new($uri_reference) if not ref $uri_reference;
511 3         511 my $fragment = $uri_reference->fragment;
512 3         19 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
513 3 50       149 return if not $resource;
514              
515 3         8 my $schema;
516 3 100 100     27 if (not length($fragment) or $fragment =~ m{^/}) {
517 2   100     26 $schema = $resource->{document}->get($resource->{path}.($fragment//''));
518             }
519             else { # we are following a URI with a plain-name fragment
520 1 50 50     16 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
521 0         0 $schema = $resource->{document}->get($subresource->{path});
522             }
523 2 100       298 return ref $schema ? dclone($schema) : $schema;
524             }
525             }
526              
527 0     1 1 0 sub get_document ($self, $uri_reference) {
  0         0  
  0         0  
  0         0  
528 0         0 my $schema_info = $self->_fetch_from_uri($uri_reference);
529 0 0       0 return if not $schema_info;
530 0         0 return $schema_info->{document};
531             }
532              
533             # defined lower down:
534             # sub add_media_type ($self, $media_type, $sub) { ... }
535             # sub get_media_type ($self, $media_type) { ... }
536             # sub add_encoding ($self, $encoding, $sub) { ... }
537             # sub get_encoding ($self, $encoding) { ... }
538             # sub add_vocabulary ($self, $classname) { ... }
539              
540             ######## NO PUBLIC INTERFACES FOLLOW THIS POINT ########
541              
542             # current spec version => { keyword => undef, or arrayref of alternatives }
543             my %removed_keywords = (
544             'draft4' => {
545             },
546             'draft6' => {
547             id => [ '$id' ],
548             },
549             'draft7' => {
550             id => [ '$id' ],
551             },
552             'draft2019-09' => {
553             id => [ '$id' ],
554             definitions => [ '$defs' ],
555             dependencies => [ qw(dependentSchemas dependentRequired) ],
556             },
557             'draft2020-12' => {
558             id => [ '$id' ],
559             definitions => [ '$defs' ],
560             dependencies => [ qw(dependentSchemas dependentRequired) ],
561             '$recursiveAnchor' => [ '$dynamicAnchor' ],
562             '$recursiveRef' => [ '$dynamicRef' ],
563             additionalItems => [ 'items' ],
564             },
565             );
566              
567             # {
568             # $spec_version => {
569             # $vocabulary_class => {
570             # traverse => [ [ $keyword => $subref ], [ ... ] ],
571             # evaluate => [ [ $keyword => $subref ], [ ... ] ],
572             # }
573             # }
574             # }
575             # If we could serialize coderefs, this could be an object attribute;
576             # otherwise, we might as well persist this for the lifetime of the process.
577             our $vocabulary_cache = {};
578              
579 42162     42163   87518 sub _traverse_subschema ($self, $schema, $state) {
  42162         82085  
  42162         72611  
  42162         74547  
  42162         67188  
580 42162         466202 delete $state->@{'keyword', grep /^_/, keys %$state};
581              
582             return E($state, 'EXCEPTION: maximum traversal depth (%d) exceeded', $self->max_traversal_depth)
583 42162 50       280157 if $state->{depth}++ > $self->max_traversal_depth;
584              
585 42162         238784 push $state->{subschemas}->@*, $state->{traversed_keyword_path}.$state->{keyword_path};
586              
587 42162         167583 my $schema_type = get_type($schema);
588             return 1 if $schema_type eq 'boolean'
589             and ($state->{specification_version} ne 'draft4'
590 42162 100 100     197743 or $state->{keyword_path} =~ m{/(?:additional(?:Items|Properties)|uniqueItems)\z});
      100        
591              
592 34451 100       104167 return E($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
593              
594 34434 100       134026 return 1 if not keys %$schema;
595              
596 33236         64569 my $valid = 1;
597 33236         273515 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
598              
599             # we use an index rather than iterating through the lists directly because the lists of
600             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
601             # and $schema keyword can make this change, and they both come first, therefore a simple index
602             # into the list is sufficient.
603             ALL_KEYWORDS:
604 33236         163036 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
605 203281         484946 my $vocabulary = $state->{vocabularies}[$vocab_index];
606 203281         288730 my $keyword_list;
607              
608 203281   66     482289 for (my $keyword_index = 0;
609             $keyword_index < ($keyword_list //= do {
610 47     47   227576 use autovivification qw(fetch store);
  47         182  
  47         324  
611             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{traverse} //= [
612             map [ $_ => $vocabulary->can('_traverse_keyword_'.($_ =~ s/^\$//r)) ],
613             $vocabulary->keywords($state->{specification_version})
614 203349   100     1006360 ];
615             })->@*;
616             $keyword_index++) {
617 1684266         3452740 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
618 1684266 100       5527376 next if not exists $schema->{$keyword};
619              
620             # keywords adjacent to $ref are not evaluated before draft2019-09
621 56837 100 100     325200 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
622              
623 56801         145467 delete $unknown_keywords{$keyword};
624 56801         152815 $state->{keyword} = $keyword;
625              
626 56801         134841 my $old_spec_version = $state->{specification_version};
627 56801         147951 my $error_count = $state->{errors}->@*;
628              
629 56801 100       246515 if (not $sub->($vocabulary, $schema, $state)) {
630             die 'traverse result is false but there are no errors (keyword: '.$keyword.')'
631 243 50       1248 if $error_count == $state->{errors}->@*;
632 243         481 $valid = 0;
633 243         1244 next;
634             }
635             warn 'traverse result is true but there are errors ('.$keyword.': '.$state->{errors}[-1]->error
636 56558 50       284177 if $error_count != $state->{errors}->@*;
637              
638             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
639 56558 100       167009 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
640              
641 56558 100       349390 if (my $callback = $state->{callbacks}{$keyword}) {
642 4         12 $error_count = $state->{errors}->@*;
643              
644 4 50       17 if (not $callback->($schema, $state)) {
645             die 'callback result is false but there are no errors (keyword: '.$keyword.')'
646 0 0       0 if $error_count == $state->{errors}->@*;
647 0         0 $valid = 0;
648 0         0 next;
649             }
650             die 'callback result is true but there are errors (keyword: '.$keyword.')'
651 4 50       4004 if $error_count != $state->{errors}->@*;
652             }
653             }
654             }
655              
656 33236         113831 delete $state->{keyword};
657              
658 33236 100 100     153679 if ($self->strict and keys %unknown_keywords) {
659 2 50       25 $valid = E($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
660             join(', ', sort keys %unknown_keywords));
661             }
662              
663             # check for previously-supported but now removed keywords
664 33236         263104 foreach my $keyword (sort keys $removed_keywords{$state->{specification_version}}->%*) {
665 103990 100       274225 next if not exists $schema->{$keyword};
666 224         1331 my $message ='no-longer-supported "'.$keyword.'" keyword present (at location "'
667             .canonical_uri($state).'")';
668 224 50       37042 if (my $alternates = $removed_keywords{$state->{specification_version}}->{$keyword}) {
669 224         1571 my @list = map '"'.$_.'"', @$alternates;
670 224 50       1091 @list = ((map $_.',', @list[0..$#list-1]), $list[-1]) if @list > 2;
671 224 100       1047 splice(@list, -1, 0, 'or') if @list > 1;
672 224         907 $message .= ': this should be rewritten as '.join(' ', @list);
673             }
674 224         87706 carp $message;
675             }
676              
677 33236         236895 return $valid;
678             }
679              
680 34827     34828   75090 sub _eval_subschema ($self, $data, $schema, $state) {
  34827         63326  
  34827         68704  
  34827         60118  
  34827         65632  
  34827         62658  
681 34827 50       99219 croak '_eval_subschema called in void context' if not defined wantarray;
682              
683             # callers created a new $state for us, so we do not propagate upwards changes to depth, traversed
684             # paths; but annotations, errors are arrayrefs so their contents will be shared
685 34827   100     212336 $state->{dynamic_scope} = [ ($state->{dynamic_scope}//[])->@* ];
686 34827         444435 delete $state->@{'keyword', grep /^_/, keys %$state};
687              
688             abort($state, 'EXCEPTION: maximum evaluation depth (%d) exceeded', $self->max_traversal_depth)
689 34827 100       231946 if $state->{depth}++ > $self->max_traversal_depth;
690              
691 34824         155041 my $schema_type = get_type($schema);
692 34824 100 66     110067 return $schema || E($state, 'subschema is false') if $schema_type eq 'boolean';
693              
694             # this should never happen, due to checks in traverse
695 33980 50       99681 abort($state, 'invalid schema type: %s', $schema_type) if $schema_type ne 'object';
696              
697 33980 100       119156 return 1 if not keys %$schema;
698              
699             # find all schema locations in effect at this data path + uri combination
700             # if any of them are absolute prefix of this schema location, we are in a loop.
701 33343         118896 my $canonical_uri = canonical_uri($state);
702 33343         140528 my $schema_location = $state->{traversed_keyword_path}.$state->{keyword_path};
703             {
704 47     47   76532 use autovivification qw(fetch store);
  47         134  
  47         303  
  33343         56827  
705             abort($state, 'EXCEPTION: infinite loop detected (same location evaluated twice)')
706             if grep substr($schema_location, 0, length) eq $_,
707 33343 100       215546 keys $state->{seen}{$state->{data_path}}{$canonical_uri}->%*;
708 33342         6199170 $state->{seen}{$state->{data_path}}{$canonical_uri}{$schema_location}++;
709             }
710              
711 33342         5265304 my $valid = 1;
712 33342         331166 my %unknown_keywords = map +($_ => undef), grep !/^x-/, keys %$schema;
713              
714             # set aside annotations collected so far; they are not used in the current scope's evaluation
715 33342         128596 my $parent_annotations = $state->{annotations};
716 33342         105735 $state->{annotations} = [];
717              
718             # in order to collect annotations from applicator keywords only when needed, we twiddle the low
719             # bit if we see a local unevaluated* keyword, and clear it again as we move on to a new data path.
720             # We also set it when _strict_schema_data is set, but only for object data instances.
721             $state->{collect_annotations} |=
722             0+((ref $data eq 'ARRAY' && exists $schema->{unevaluatedItems})
723             || ((my $is_object_data = ref $data eq 'HASH')
724 33342   100     393814 && (exists $schema->{unevaluatedProperties} || !!$state->{seen_data_properties})));
725              
726             # set aside defaults collected so far; we need to keep the subschema's defaults separated in
727             # case they must be discarded due to overall invalidity of the subschema
728 33342         99491 my $defaults = $state->{defaults};
729 33342 100       119273 $state->{defaults} = {} if $state->{defaults};
730              
731             # we use an index rather than iterating through the lists directly because the lists of
732             # vocabularies and keywords can change after we have started. However, only the Core vocabulary
733             # and $schema keyword can make this change, and they both come first, therefore a simple index
734             # into the list is sufficient.
735              
736             ALL_KEYWORDS:
737 33342         151650 for (my $vocab_index = 0; $vocab_index < $state->{vocabularies}->@*; $vocab_index++) {
738 183009         425923 my $vocabulary = $state->{vocabularies}[$vocab_index];
739 183009         265860 my $keyword_list;
740              
741 183009   66     421038 for (my $keyword_index = 0;
742             $keyword_index < ($keyword_list //= do {
743 47     47   23017 use autovivification qw(fetch store);
  47         202  
  47         295  
744             $vocabulary_cache->{$state->{specification_version}}{$vocabulary}{evaluate} //= [
745             map [ $_ => $vocabulary->can('_eval_keyword_'.($_ =~ s/^\$//r)) ],
746             $vocabulary->keywords($state->{specification_version})
747 183012   100     958765 ];
748             })->@*;
749             $keyword_index++) {
750 1509814         3115545 my ($keyword, $sub) = $keyword_list->[$keyword_index]->@*;
751 1509814 100       4898074 next if not exists $schema->{$keyword};
752              
753             # keywords adjacent to $ref are not evaluated before draft2019-09
754 60016 100 100     346762 next if $keyword ne '$ref' and exists $schema->{'$ref'} and $state->{specification_version} =~ /^draft[467]\z/;
      100        
755              
756 59985         160993 delete $unknown_keywords{$keyword};
757 59985 100 100     171135 next if not $valid and $state->{short_circuit} and $state->{strict};
      66        
758              
759 59984         183563 $state->{keyword} = $keyword;
760              
761 59984 100       136178 if ($sub) {
762 55908         140802 my $old_spec_version = $state->{specification_version};
763 55908         144877 my $error_count = $state->{errors}->@*;
764              
765 55908         132413 try {
766 55908 100       269788 if (not $sub->($vocabulary, $data, $schema, $state)) {
767             warn 'evaluation result is false but there are no errors (keyword: '.$keyword.')'
768 13161 50       65714 if $error_count == $state->{errors}->@*;
769 13161         26442 $valid = 0;
770              
771 13161 100 100     87903 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
772 6948         42871 next;
773             }
774              
775             warn 'evaluation result is true but there are errors (keyword: '.$keyword.')'
776 42703 50       468155 if $error_count != $state->{errors}->@*;
777             }
778             catch ($e) {
779 44 100       1036 die $e if $e->$_isa('JSON::Schema::Modern::Error');
780 2         37 abort($state, 'EXCEPTION: '.$e);
781             }
782              
783             # a keyword changed the keyword list for this vocabulary; re-fetch the list before continuing
784 42703 100       168543 undef $keyword_list if $state->{specification_version} ne $old_spec_version;
785             }
786              
787 46779 100 100     287646 if (my $callback = ($state->{callbacks}//{})->{$keyword}) {
788 19         30 my $error_count = $state->{errors}->@*;
789              
790 19 100       57 if (not $callback->($data, $schema, $state)) {
791             warn 'callback result is false but there are no errors (keyword: '.$keyword.')'
792 2 50       10 if $error_count == $state->{errors}->@*;
793 2         5 $valid = 0;
794              
795 2 100 66     20 last ALL_KEYWORDS if $state->{short_circuit} and not $state->{strict};
796 1         9 next;
797             }
798             warn 'callback result is true but there are errors (keyword: '.$keyword.')'
799 17 50       158 if $error_count != $state->{errors}->@*;
800             }
801             }
802             }
803              
804 33298         114303 delete $state->{keyword};
805              
806 33298 100 100     107306 if ($state->{strict} and keys %unknown_keywords) {
807 3 100       33 abort($state, 'unknown keyword%s seen in schema: %s', keys %unknown_keywords > 1 ? 's' : '',
808             join(', ', sort keys %unknown_keywords));
809             }
810              
811             # Note: we can remove all of this entirely and just rely on strict mode when we (eventually!) remove
812             # the traverse phase and replace with evaluate-against-metaschema.
813 33295 100 100     124049 if ($state->{seen_data_properties} and $is_object_data) {
814             # record the locations of all local properties
815             $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 0
816 156         1564 foreach grep !/^x-/, keys %$data;
817              
818             my @evaluated_properties = map {
819 156         533 my $keyword = $_->{keyword};
  577         1123  
820             (grep $keyword eq $_, qw(properties additionalProperties patternProperties unevaluatedProperties))
821 577 100       2045 ? $_->{annotation}->@* : ();
822             } local_annotations($state);
823              
824             # tick off properties that were recognized by this subschema
825 156         497 $state->{seen_data_properties}{jsonp($state->{data_path}, $_)} |= 1 foreach @evaluated_properties;
826              
827             # weird! the draft4 metaschema doesn't know about '$ref' at all!
828             $state->{seen_data_properties}{$state->{data_path}.'/$ref'} |= 1
829 156 100 66     610 if exists $data->{'$ref'} and $state->{specification_version} eq 'draft4';
830             }
831              
832 33295 100 100     159595 if ($valid and $state->{collect_annotations} and $state->{specification_version} !~ /^draft(?:[467]|2019-09)\z/) {
      100        
833             annotate_self(+{ %$state, keyword => $_, _unknown => 1 }, $schema)
834 969         3957 foreach sort keys %unknown_keywords;
835             }
836              
837             # only keep new annotations if schema is valid
838 33295 100       117240 push $parent_annotations->@*, $state->{annotations}->@* if $valid;
839              
840             # only keep new defaults if schema is valid
841             $defaults->@{keys $state->{defaults}->%*} = values $state->{defaults}->%*
842 33295 100 100     131576 if $valid and $state->{defaults};
843              
844 33295         369903 return $valid;
845             }
846              
847             has _resource_index => (
848             is => 'bare',
849             isa => Map[my $resource_key_type = Str->where('!/#/'), my $resource_type = Dict[
850             canonical_uri => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
851             path => json_pointer_type, # JSON pointer relative to the document root
852             specification_version => my $spec_version_type = Enum(SPECIFICATION_VERSIONS_SUPPORTED),
853             document => InstanceOf['JSON::Schema::Modern::Document'],
854             # the vocabularies used when evaluating instance data against schema
855             vocabularies => ArrayRef[my $vocabulary_class_type = ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
856             anchors => Optional[HashRef[Dict[
857             canonical_uri => canonical_uri_type, # equivalent uri with json pointer fragment
858             path => json_pointer_type, # JSON pointer relative to the document root
859             dynamic => Optional[Bool],
860             ]]],
861             Slurpy[HashRef[Undef]], # no other fields allowed
862             ]],
863             );
864              
865             sub _get_resource {
866 44029 50   44030   166367 die 'bad resource: ', $_[1] if $_[1] =~ /#/;
867 44029   100     4275585 ($_[0]->{_resource_index}//{})->{$_[1]}
868             }
869              
870             # does not check for duplicate entries, or for malformed uris
871             sub _add_resources_unsafe {
872 47     47   102860 use autovivification 'store';
  47         175  
  47         337  
873             $_[0]->{_resource_index}{$resource_key_type->($_->[0])} = $resource_type->($_->[1])
874 103     104   1627 foreach pairs @_[1..$#_];
875             }
876 25   50 26   109341 sub _resource_index { ($_[0]->{_resource_index}//{})->%* }
877 17393   100 17394   4305099 sub _canonical_resources { values(($_[0]->{_resource_index}//{})->%*) }
878 2121   50 2122   374442 sub _resource_pairs { pairs(($_[0]->{_resource_index}//{})->%*) }
879              
880 18974     18975   72498 sub _add_resource ($self, @kvs) {
  18974         41137  
  18974         50952  
  18974         39471  
881 18974         123187 foreach my $pair (sort { $a->[0] cmp $b->[0] } pairs @kvs) {
  0         0  
882 18974         55433 my ($canonical_uri, $resource) = @$pair;
883              
884 18974 100       87874 if (my $existing = $self->_get_resource($canonical_uri)) {
    100          
885             # we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of schemas that
886             # lack all identifiers altogether, but preserve other resources from the original document
887 17055 100       83246 if ($canonical_uri ne '') {
888             my @diffs = (
889             ($existing->{path} eq $resource->{path} ? () : 'path'),
890             ($existing->{canonical_uri} eq $resource->{canonical_uri} ? () : 'canonical_uri'),
891             ($existing->{specification_version} eq $resource->{specification_version} ? () : 'specification_version'),
892 859 100       9398 (refaddr($existing->{document}) == refaddr($resource->{document}) ? () : 'refaddr'));
    100          
    50          
    100          
893 859 100       386904 next if not @diffs;
894 10         3578 croak 'uri "'.$canonical_uri.'" conflicts with an existing schema resource: documents differ by ',
895             join(', ', @diffs);
896             }
897             }
898             elsif (JSON::Schema::Modern::Utilities::get_schema_filename($canonical_uri)) {
899 2         770 croak 'uri "'.$canonical_uri.'" conflicts with an existing cached schema resource';
900             }
901              
902 47     47   36033 use autovivification 'store';
  47         127  
  47         307  
903 18113         116043 $self->{_resource_index}{$resource_key_type->($canonical_uri)} = $resource_type->($resource);
904             }
905             }
906              
907             # $vocabulary uri (not its $id!) => [ specification_version, class ]
908             has _vocabulary_classes => (
909             is => 'bare',
910             isa => HashRef[
911             my $vocabulary_type = Tuple[
912             $spec_version_type,
913             $vocabulary_class_type,
914             ]
915             ],
916             reader => '__vocabulary_classes',
917             lazy => 1,
918             default => sub {
919             +{
920 12     13   1104 map { my $class = $_; pairmap { $a => [ $b, $class ] } $class->vocabulary }
  12     13   33  
  12     13   372  
  12     12   830  
  12     12   42  
  12     12   316  
  12     12   805  
  12     12   42  
  12     7   284  
  12     7   842  
  12     7   29  
  12     7   256  
  12     7   832  
  12     7   29  
  12     7   242  
  12     7   727  
  12     1   25  
  12     1   238  
  12     1   57  
  12     1   26  
  12     1   220  
  12     1   56  
  12         26  
  12         50  
  7         79  
  7         35  
  7         261  
  7         66  
  7         18  
  7         154  
  7         38  
  7         15  
  7         158  
  7         36  
  7         47  
  7         134  
  7         35  
  7         16  
  7         197  
  7         36  
  7         29  
  7         171  
  7         37  
  7         17  
  7         116  
  7         35  
  7         17  
  7         38  
  1         4  
  1         3  
  1         10  
  1         3  
  1         2  
  1         31  
  1         5  
  1         2  
  1         13  
  1         3  
  1         2  
  1         9  
  1         2  
  1         1  
  1         9  
  1         3  
  1         2  
  1         4  
921             map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
922             qw(Core Applicator Validation FormatAssertion FormatAnnotation Content MetaData Unevaluated)
923             }
924             },
925             );
926              
927 107     108   3747 sub _get_vocabulary_class { $_[0]->__vocabulary_classes->{$_[1]} }
928              
929 10     11 1 46343 sub add_vocabulary ($self, $classname) {
  10         35  
  10         28  
  10         22  
930 10 50       417 return if grep $_->[1] eq $classname, values $self->__vocabulary_classes->%*;
931              
932 10         1616 $vocabulary_class_type->(load_module($classname));
933              
934             # uri => version, uri => version
935 7         1023 foreach my $pair (pairs $classname->vocabulary) {
936 7         135 my ($uri_string, $spec_version) = @$pair;
937 7         47 Str->where(q{my $uri = Mojo::URL->new($_); $uri->is_abs && !defined $uri->fragment})->($uri_string);
938 6         12868 $spec_version_type->($spec_version);
939              
940 4 100       973 croak 'keywords starting with "$" are reserved for core and cannot be used'
941             if grep /^\$/, $classname->keywords;
942              
943 3         45 $self->{_vocabulary_classes}{$uri_string} = $vocabulary_type->([ $spec_version, $classname ]);
944             }
945             }
946              
947             # $schema uri => [ specification_version, [ vocab classes, in evaluation order ] ].
948             has _metaschema_vocabulary_classes => (
949             is => 'bare',
950             isa => HashRef[
951             my $mvc_type = Tuple[
952             $spec_version_type,
953             ArrayRef[$vocabulary_class_type],
954             ]
955             ],
956             reader => '__metaschema_vocabulary_classes',
957             lazy => 1,
958             default => sub {
959 38     38   25803 my @modules = map load_module('JSON::Schema::Modern::Vocabulary::'.$_),
  38     38   158  
  38     38   1497  
  38     38   22090  
  38     38   231  
  38     38   1715  
  38     38   27952  
  38     30   243  
  38     30   1727  
  38     30   26879  
  38     30   244  
  38     30   1576  
  38     30   25261  
  38     30   222  
  38     19   1494  
  38     19   22944  
  38     19   200  
  38     19   1485  
  38     19   286  
  38     19   103  
  38     19   260  
  30     16   290  
  30     15   70  
  30     15   896  
  30     15   160  
  30     15   57  
  30     15   566  
  30     15   189  
  30     13   63  
  30     13   564  
  30     13   167  
  30     13   64  
  30     13   1815  
  30     13   543  
  30     13   67  
  30     12   636  
  30     12   1362  
  30     12   96  
  30     12   649  
  30     12   130  
  30     12   68  
  30     12   205  
  19     11   270  
  19     11   61  
  19     11   549  
  19     11   117  
  19     11   43  
  19     11   481  
  19     11   123  
  19     10   47  
  19     10   414  
  19     10   103  
  19     9   41  
  19     9   442  
  19     9   103  
  19     9   45  
  19     9   463  
  19     9   103  
  19     9   43  
  19     9   375  
  19     9   97  
  19     9   42  
  19     9   162  
  16     9   155  
  16     9   39  
  16     9   329  
  15     9   77  
  15     9   32  
  15     9   304  
  15     9   86  
  15     8   40  
  15     8   297  
  15     8   77  
  15     8   31  
  15     8   221  
  15     8   78  
  15     8   33  
  15     6   460  
  15     6   86  
  15     6   35  
  15     6   316  
  15     6   71  
  15     6   74  
  15     6   136  
  13     6   129  
  13     6   39  
  13     6   334  
  13     6   93  
  13     6   34  
  13     6   416  
  13     6   93  
  13     6   34  
  13     6   313  
  13     6   76  
  13     5   30  
  13     5   295  
  13     5   72  
  13     5   29  
  13     5   256  
  13     5   69  
  13     5   31  
  13     5   384  
  13     5   74  
  13     5   30  
  13     5   127  
  12     6   116  
  12     6   33  
  12     6   386  
  12     6   90  
  12     6   31  
  12     6   246  
  12     6   77  
  12     6   25  
  12     4   1242  
  12     4   92  
  12     4   27  
  12     4   281  
  12     4   68  
  12     4   23  
  12     4   300  
  12     4   68  
  12     4   31  
  12     4   272  
  12     4   62  
  12     4   30  
  12     4   106  
  11         100  
  11         30  
  11         319  
  11         59  
  11         27  
  11         207  
  11         70  
  11         28  
  11         280  
  11         56  
  11         26  
  11         334  
  11         76  
  11         23  
  11         243  
  11         55  
  11         24  
  11         223  
  11         54  
  11         24  
  11         83  
  10         136  
  10         27  
  10         218  
  10         52  
  10         25  
  10         224  
  9         57  
  9         21  
  9         155  
  9         66  
  9         22  
  9         254  
  9         53  
  9         20  
  9         184  
  9         48  
  9         20  
  9         216  
  9         50  
  9         24  
  9         60  
  9         106  
  9         23  
  9         253  
  9         52  
  9         22  
  9         212  
  9         53  
  9         18  
  9         160  
  9         45  
  9         20  
  9         1180  
  9         50  
  9         22  
  9         184  
  9         44  
  9         20  
  9         198  
  9         42  
  9         20  
  9         101  
  9         100  
  9         21  
  9         188  
  9         44  
  9         18  
  9         203  
  9         47  
  9         17  
  9         144  
  9         40  
  9         19  
  9         157  
  9         40  
  9         18  
  9         178  
  9         40  
  9         17  
  9         430  
  9         45  
  9         20  
  9         52  
  8         77  
  8         45  
  8         212  
  8         43  
  8         16  
  8         266  
  8         47  
  8         15  
  8         141  
  8         33  
  8         20  
  8         153  
  8         41  
  8         39  
  8         131  
  8         35  
  8         20  
  8         171  
  8         37  
  8         16  
  8         54  
  6         51  
  6         15  
  6         142  
  6         30  
  6         16  
  6         118  
  6         37  
  6         14  
  6         133  
  6         33  
  6         13  
  6         124  
  6         30  
  6         16  
  6         125  
  6         32  
  6         16  
  6         137  
  6         31  
  6         14  
  6         57  
  6         56  
  6         17  
  6         167  
  6         35  
  6         13  
  6         129  
  6         39  
  6         16  
  6         112  
  6         30  
  6         14  
  6         108  
  6         33  
  6         13  
  6         151  
  6         34  
  6         15  
  6         121  
  6         31  
  6         13  
  6         44  
  6         768  
  6         6850  
  6         235  
  6         750  
  6         6678  
  6         121  
  6         618  
  6         6096  
  6         125  
  5         30  
  5         10  
  5         186  
  5         32  
  5         13  
  5         107  
  5         29  
  5         11  
  5         92  
  5         58  
  5         14  
  5         123  
  5         50  
  5         12  
  5         129  
  5         27  
  5         14  
  5         73  
  5         30  
  5         13  
  5         111  
  5         27  
  5         12  
  5         153  
  5         31  
  5         9  
  5         100  
  5         45  
  5         11  
  5         83  
  5         27  
  5         11  
  5         36  
  6         45  
  6         16  
  6         146  
  6         31  
  6         13  
  6         113  
  6         51  
  6         15  
  6         115  
  6         27  
  6         11  
  6         126  
  6         28  
  6         13  
  6         103  
  6         28  
  6         13  
  6         113  
  6         28  
  6         14  
  6         58  
  6         80  
  6         13  
  6         124  
  4         21  
  4         7  
  4         63  
  4         22  
  4         8  
  4         79  
  4         19  
  4         8  
  4         62  
  4         18  
  4         7  
  4         69  
  4         18  
  4         8  
  4         98  
  4         18  
  4         9  
  4         31  
  4         26  
  4         8  
  4         62  
  4         18  
  4         9  
  4         47  
  4         22  
  4         8  
  4         80  
  4         18  
  4         9  
  4         97  
  4         21  
  4         8  
  4         72  
  4         18  
  4         10  
  4         71  
  4         15  
  4         9  
  4         26  
960             qw(Core Validation FormatAnnotation Applicator Content MetaData Unevaluated);
961             +{
962             'https://json-schema.org/draft/2020-12/schema' => [ 'draft2020-12', [ @modules ] ],
963             do { pop @modules; () }, # remove Unevaluated
964             'https://json-schema.org/draft/2019-09/schema' => [ 'draft2019-09', [ @modules ] ],
965             'http://json-schema.org/draft-07/schema' => [ 'draft7', [ @modules ] ],
966             do { splice @modules, 4, 1; () }, # remove Content
967             'http://json-schema.org/draft-06/schema' => [ 'draft6', \@modules ],
968             'http://json-schema.org/draft-04/schema' => [ 'draft4', \@modules ],
969             },
970             },
971             );
972              
973 30501     30502   996387 sub _get_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} }
974 5924     5925   61308 sub _set_metaschema_vocabulary_classes { $_[0]->__metaschema_vocabulary_classes->{$_[1] =~ s/#\z//r} = $mvc_type->($_[2]) }
975 4     5   260 sub __all_metaschema_vocabulary_classes { values $_[0]->__metaschema_vocabulary_classes->%* }
976              
977             # translate vocabulary URIs into classes, caching the results (if any)
978 53     54   194 sub _fetch_vocabulary_data ($self, $state, $schema_info) {
  53         151  
  53         128  
  53         98  
  53         123  
979 53 100       440 if (not exists $schema_info->{schema}{'$vocabulary'}) {
980             # "If "$vocabulary" is absent, an implementation MAY determine behavior based on the meta-schema
981             # if it is recognized from the URI value of the referring schema's "$schema" keyword."
982 2         36 my $metaschema_uri = $self->METASCHEMA_URIS->{$schema_info->{specification_version}};
983 2         10 return $self->_get_metaschema_vocabulary_classes($metaschema_uri)->@*;
984             }
985              
986 51         123 my $valid = 1;
987             # Core ยง8.1.2-6: "The "$vocabulary" keyword SHOULD be used in the root schema of any schema
988             # document intended for use as a meta-schema. It MUST NOT appear in subschemas."
989 51 100       302 $valid = E($state, '$vocabulary can only appear at the document root') if length $schema_info->{document_path};
990 51 100       374 $valid = E($state, 'metaschemas must have an $id') if not exists $schema_info->{schema}{'$id'};
991              
992 51 100       241 return (undef, []) if not $valid;
993              
994 49         121 my @vocabulary_classes;
995              
996 49         404 foreach my $uri (sort keys $schema_info->{schema}{'$vocabulary'}->%*) {
997 105         445 my $class_info = $self->_get_vocabulary_class($uri);
998             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" is not a known vocabulary', $uri), next
999 105 100 100     17092 if $schema_info->{schema}{'$vocabulary'}{$uri} and not $class_info;
1000              
1001 97 100       1313 next if not $class_info; # vocabulary is not known, but marked as false in the metaschema
1002              
1003 89         274 my ($spec_version, $class) = @$class_info;
1004             $valid = E({ %$state, _keyword_path_suffix => $uri }, '"%s" uses %s, but the metaschema itself uses %s',
1005             $uri, $spec_version, $schema_info->{specification_version}), next
1006 89 100       549 if $spec_version ne $schema_info->{specification_version};
1007              
1008 83         296 push @vocabulary_classes, $class;
1009             }
1010              
1011             @vocabulary_classes = sort {
1012 49 50       278 $a->evaluation_order <=> $b->evaluation_order
  49 50       269  
1013             || ($a->evaluation_order == 999 ? 0
1014             : ($valid = E($state, '%s and %s have a conflicting evaluation_order', sort $a, $b)))
1015             } @vocabulary_classes;
1016              
1017 49 100 100     305 $valid = E($state, 'the first vocabulary (by evaluation_order) must be Core')
1018             if ($vocabulary_classes[0]//'') ne 'JSON::Schema::Modern::Vocabulary::Core';
1019              
1020 49         132 my %seen_keyword;
1021 49         137 foreach my $class (@vocabulary_classes) {
1022 83         523 foreach my $keyword ($class->keywords($schema_info->{specification_version})) {
1023             $valid = E($state, '%s keyword "%s" conflicts with keyword of the same name from %s',
1024             $class, $keyword, $seen_keyword{$keyword})
1025 794 100       1636 if $seen_keyword{$keyword};
1026 794         2015 $seen_keyword{$keyword} = $class;
1027             }
1028             }
1029              
1030 49 100       818 return ($schema_info->{specification_version}, $valid ? \@vocabulary_classes : []);
1031             }
1032              
1033             # used for determining a default '$schema' keyword where there is none
1034             # these are also normalized as this is how we cache them
1035 47         10144 use constant METASCHEMA_URIS => {
1036             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
1037             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
1038             'draft7' => 'http://json-schema.org/draft-07/schema',
1039             'draft6' => 'http://json-schema.org/draft-06/schema',
1040             'draft4' => 'http://json-schema.org/draft-04/schema',
1041 47     47   106958 };
  47         135  
1042              
1043             # for internal use only. files are under share/
1044 47         79705 use constant _CACHED_METASCHEMAS => {
1045             'https://json-schema.org/draft/2020-12/meta/applicator' => 'draft2020-12/meta/applicator.json',
1046             'https://json-schema.org/draft/2020-12/meta/content' => 'draft2020-12/meta/content.json',
1047             'https://json-schema.org/draft/2020-12/meta/core' => 'draft2020-12/meta/core.json',
1048             'https://json-schema.org/draft/2020-12/meta/format-annotation' => 'draft2020-12/meta/format-annotation.json',
1049             'https://json-schema.org/draft/2020-12/meta/format-assertion' => 'draft2020-12/meta/format-assertion.json',
1050             'https://json-schema.org/draft/2020-12/meta/meta-data' => 'draft2020-12/meta/meta-data.json',
1051             'https://json-schema.org/draft/2020-12/meta/unevaluated' => 'draft2020-12/meta/unevaluated.json',
1052             'https://json-schema.org/draft/2020-12/meta/validation' => 'draft2020-12/meta/validation.json',
1053             'https://json-schema.org/draft/2020-12/output/schema' => 'draft2020-12/output/schema.json',
1054             'https://json-schema.org/draft/2020-12/schema' => 'draft2020-12/schema.json',
1055              
1056             'https://json-schema.org/draft/2019-09/meta/applicator' => 'draft2019-09/meta/applicator.json',
1057             'https://json-schema.org/draft/2019-09/meta/content' => 'draft2019-09/meta/content.json',
1058             'https://json-schema.org/draft/2019-09/meta/core' => 'draft2019-09/meta/core.json',
1059             'https://json-schema.org/draft/2019-09/meta/format' => 'draft2019-09/meta/format.json',
1060             'https://json-schema.org/draft/2019-09/meta/meta-data' => 'draft2019-09/meta/meta-data.json',
1061             'https://json-schema.org/draft/2019-09/meta/validation' => 'draft2019-09/meta/validation.json',
1062             'https://json-schema.org/draft/2019-09/output/schema' => 'draft2019-09/output/schema.json',
1063             'https://json-schema.org/draft/2019-09/schema' => 'draft2019-09/schema.json',
1064              
1065             # trailing # is omitted because we always cache documents by its canonical (fragmentless) URI
1066             'http://json-schema.org/draft-07/schema' => 'draft7/schema.json',
1067             'http://json-schema.org/draft-06/schema' => 'draft6/schema.json',
1068             'http://json-schema.org/draft-04/schema' => 'draft4/schema.json',
1069 47     47   403 };
  47         122  
1070              
1071             # simple runtime-wide cache of metaschema document objects that are sourced from disk
1072             my $metaschema_cache = {};
1073              
1074             {
1075             my $share_dir = dist_dir('JSON-Schema-Modern');
1076             JSON::Schema::Modern::Utilities::register_schema($_, $share_dir.'/'._CACHED_METASCHEMAS->{$_})
1077             foreach keys _CACHED_METASCHEMAS->%*;
1078             }
1079              
1080             # returns the same as _get_resource
1081 24424     24425   1420289 sub _get_or_load_resource ($self, $uri) {
  24424         51674  
  24424         46075  
  24424         43245  
1082 24424         86869 my $resource = $self->_get_resource($uri);
1083 24424 100       3388701 return $resource if $resource;
1084              
1085 114 100       699 if (my $document = load_cached_document($self, $uri)) {
1086 103         566 return $self->_get_resource($uri);
1087             }
1088              
1089             # TODO:
1090             # - load from network or disk
1091              
1092 11         44 return;
1093             };
1094              
1095             # returns information necessary to use a schema found at a particular URI or uri-reference:
1096             # - schema: a schema (which may not be at a document root)
1097             # - canonical_uri: the canonical uri for that schema,
1098             # - document: the JSON::Schema::Modern::Document object that holds that schema
1099             # - document_path: the path relative to the document root for this schema
1100             # - specification_version: the specification version that applies to this schema
1101             # - vocabularies: the vocabularies to use when considering schema keywords
1102             # creates a Document and adds it to the resource index, if not already present.
1103 24186     24187   337314 sub _fetch_from_uri ($self, $uri_reference) {
  24186         49386  
  24186         52395  
  24186         42761  
1104 24186 50       112275 $uri_reference = Mojo::URL->new($uri_reference) if not is_schema($uri_reference);
1105              
1106             # this is *a* resource that would contain our desired location, but may not be the closest one
1107 24186         8831920 my $resource = $self->_get_or_load_resource($uri_reference->clone->fragment(undef));
1108 24186 100       201023 return if not $resource;
1109              
1110 24175         72903 my $fragment = $uri_reference->fragment;
1111 24175 100 100     182520 if (not length($fragment) or $fragment =~ m{^/}) {
1112 23595   100     290166 my $subschema = $resource->{document}->get(my $document_path = $resource->{path}.($fragment//''));
1113 23595 100       275936 return if not defined $subschema;
1114              
1115 23593         48067 my $closest_resource;
1116 23593 100       67932 if (not length $fragment) { # we already have the canonical resource root
1117 21472         59601 $closest_resource = [ undef, $resource ];
1118             }
1119             else {
1120             # determine the canonical uri by finding the closest schema resource(s)
1121 2121         8164 my $doc_addr = refaddr($resource->{document});
1122             my @closest_resources =
1123 506         3953 sort { length($b->[1]{path}) <=> length($a->[1]{path}) } # sort by length, descending
1124             grep { !length($_->[1]{path}) # document root
1125 2909 100 66     38003 || length($document_path)
1126             && $document_path =~ m{^\Q$_->[1]{path}\E(?:/|\z)} } # path is above desired location
1127 2121         9788 grep { refaddr($_->[1]{document}) == $doc_addr } # in same document
  228326         570071  
1128             $self->_resource_pairs;
1129              
1130             # now whittle down to all the resources with the same document path as the first candidate
1131 2121 100       63882 if (@closest_resources > 1) {
1132             # find the resource key that most closely matches the original query uri, by matching prefixes
1133 462         2968 my $match = $uri_reference.'';
1134             @closest_resources =
1135 24         133 sort { _prefix_match_length($b->[0], $match) <=> _prefix_match_length($a->[0], $match) }
1136             grep $_->[1]{path} eq $closest_resources[0]->[1]{path},
1137 462         127681 @closest_resources;
1138             }
1139              
1140 2121         6469 $closest_resource = $closest_resources[0];
1141             }
1142              
1143             my $canonical_uri = $closest_resource->[1]{canonical_uri}->clone
1144 23593         159497 ->fragment(substr($document_path, length($closest_resource->[1]{path})));
1145 23593 100       2397501 $canonical_uri->fragment(undef) if not length($canonical_uri->fragment);
1146              
1147             return {
1148             schema => $subschema,
1149             canonical_uri => $canonical_uri,
1150             document_path => $document_path,
1151 23593         547755 $closest_resource->[1]->%{qw(document specification_version vocabularies)}, # reference, not copy
1152             };
1153             }
1154             else { # we are following a URI with a plain-name fragment
1155 580 100 100     4710 return if not my $subresource = ($resource->{anchors}//{})->{$fragment};
1156             return {
1157             schema => $resource->{document}->get($subresource->{path}),
1158             canonical_uri => $subresource->{canonical_uri}, # this is *not* the anchor-containing URI
1159             document_path => $subresource->{path},
1160 578         4687 $resource->%{qw(document specification_version vocabularies)}, # reference, not copy
1161             };
1162             }
1163             }
1164              
1165             # given two strings, determines the number of characters in common, starting from the first
1166             # character
1167 48     49   138 sub _prefix_match_length ($x, $y) {
  48         96  
  48         84  
  48         72  
1168 48         170 my $len = min(length($x), length($y));
1169 48         152 foreach my $pos (0..$len) {
1170 1406 100       3418 return $pos if substr($x, $pos, 1) ne substr($y, $pos, 1);
1171             }
1172 0         0 return $len;
1173             }
1174              
1175             # Mojo::JSON::JSON_XS is false when the environment variable $MOJO_NO_JSON_XS is set
1176             # and also checks if Cpanel::JSON::XS is installed.
1177             # Mojo::JSON falls back to its own pure-perl encoder/decoder but does not support all the options
1178             # that we require here.
1179             use constant _JSON_BACKEND =>
1180 47         1043 Mojo::JSON::JSON_XS && eval { Cpanel::JSON::XS->VERSION('4.38'); 1 } ? 'Cpanel::JSON::XS'
  47         96920  
1181 47 0       121 : eval { JSON::PP->VERSION('4.11'); 1 } ? 'JSON::PP'
  2 50       4  
  2         26  
1182 47     47   621 : die 'Cpanel::JSON::XS 4.38 or JSON::PP 4.11 is required';
  47         120  
1183              
1184             # used for internal encoding as well (when caching serialized schemas)
1185             has _json_decoder => (
1186             is => 'ro',
1187             isa => HasMethods[qw(encode decode)],
1188             lazy => 1,
1189             default => sub { _JSON_BACKEND->new->allow_nonref(1)->canonical(1)->utf8(1)->allow_bignum(1)->convert_blessed(1) },
1190             );
1191              
1192             # since media types are case-insensitive, all type names must be casefolded on insertion.
1193             has _media_type => (
1194             is => 'bare',
1195             isa => my $media_type_type = Map[Str->where(q{$_ eq CORE::fc($_)}), CodeRef],
1196             reader => '__media_type',
1197             lazy => 1,
1198             default => sub ($self) {
1199             my $_json_media_type = sub ($content_ref) {
1200             # utf-8 decoding is always done, as per the JSON spec.
1201             # other charsets are not supported: see RFC8259 ยง11
1202             \ _JSON_BACKEND->new->allow_nonref(1)->utf8(1)->decode($content_ref->$*);
1203             };
1204             +{
1205             (map +($_ => $_json_media_type),
1206             qw(application/json application/schema+json application/schema-instance+json)),
1207             (map +($_ => sub ($content_ref) { $content_ref }),
1208             qw(text/* application/octet-stream)),
1209             'application/x-www-form-urlencoded' => sub ($content_ref) {
1210             \ Mojo::Parameters->new->charset('UTF-8')->parse($content_ref->$*)->to_hash;
1211             },
1212             'application/x-ndjson' => sub ($content_ref) {
1213             my $decoder = _JSON_BACKEND->new->allow_nonref(1)->utf8(1);
1214             my $line = 0; # line numbers start at 1
1215             \[ map {
1216             do {
1217             try { ++$line; $decoder->decode($_) }
1218             catch ($e) { die 'parse error at line '.$line.': '.$e }
1219             }
1220             }
1221             split(/\r?\n/, $content_ref->$*)
1222             ];
1223             },
1224             };
1225             },
1226             );
1227              
1228 5     6 1 4948 sub add_media_type { $media_type_type->({ @_[1..2] }); $_[0]->__media_type->{$_[1]} = $_[2]; }
  4         504  
1229              
1230             # get_media_type('TExT/bloop') will fall through to matching an entry for 'text/*' or '*/*'
1231 38     39 1 19220 sub get_media_type ($self, $type) {
  38         90  
  38         111  
  38         82  
1232 38         1194 my $types = $self->__media_type;
1233 38         1052 my $mt = $types->{fc $type};
1234 38 100       187 return $mt if $mt;
1235              
1236 9 100 100 50   105 return $types->{(first { m{([^/]+)/\*\z} && fc($type) =~ m{^\Q$1\E/[^/]+\z} } keys %$types) // '*/*'};
  49         577  
1237             };
1238              
1239             has _encoding => (
1240             is => 'bare',
1241             isa => HashRef[CodeRef],
1242             reader => '__encoding',
1243             lazy => 1,
1244             default => sub ($self) {
1245             +{
1246             identity => sub ($content_ref) { $content_ref },
1247             base64 => sub ($content_ref) {
1248             die "invalid characters\n"
1249             if $content_ref->$* =~ m{[^A-Za-z0-9+/=]} or $content_ref->$* =~ m{=(?=[^=])};
1250             require MIME::Base64; \ MIME::Base64::decode_base64($content_ref->$*);
1251             },
1252             base64url => 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_base64url($content_ref->$*);
1256             },
1257             };
1258             },
1259             );
1260              
1261 22     23 1 918 sub get_encoding { $_[0]->__encoding->{$_[1]} }
1262 0     1 1 0 sub add_encoding { $_[0]->__encoding->{$_[1]} = CodeRef->($_[2]) }
1263              
1264             # callback hook for Sereal::Encoder
1265 3     4 0 3514 sub FREEZE ($self, $serializer) {
  3         8  
  3         7  
  3         6  
1266 3         53 my $data = +{ %$self };
1267             # Cpanel::JSON::XS doesn't serialize: https://github.com/Sereal/Sereal/issues/266
1268             # coderefs can't serialize cleanly and must be re-added by the user.
1269 3         25 delete $data->@{qw(_json_decoder _format_validations _media_type _encoding)};
1270 3         95 return $data;
1271             }
1272              
1273             # callback hook for Sereal::Decoder
1274 4     5 0 2125 sub THAW ($class, $serializer, $data) {
  4         11  
  4         14  
  4         8  
  4         8  
1275 4         11 my $self = bless($data, $class);
1276              
1277             # load all vocabulary classes, both those used by loaded schemas, as well as all the core modules
1278             load_module($_)
1279 4         23 foreach uniq(
1280             (map $_->{vocabularies}->@*, $self->_canonical_resources),
1281             (map $_->[1], values $self->__vocabulary_classes->%*));
1282              
1283 4         194 return $self;
1284             }
1285              
1286             1;
1287              
1288             __END__