File Coverage

blib/lib/JSON/Schema/Modern/Document.pm
Criterion Covered Total %
statement 90 91 98.9
branch 16 18 88.8
condition 18 23 78.2
subroutine 25 26 96.1
pod 3 6 50.0
total 152 164 92.6


line stmt bran cond sub pod time code
1 34     34   788 use strict;
  34         124  
  34         1151  
2 34     34   201 use warnings;
  34         100  
  34         1757  
3             package JSON::Schema::Modern::Document;
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: One JSON Schema document
6              
7             our $VERSION = '0.570';
8              
9 34     34   685 use 5.020;
  34         172  
10 34     34   228 use Moo;
  34         90  
  34         267  
11 34     34   13171 use strictures 2;
  34         336  
  34         1890  
12 34     34   6824 use stable 0.031 'postderef';
  34         685  
  34         252  
13 34     34   5507 use experimental 'signatures';
  34         144  
  34         204  
14 34     34   2924 use if "$]" >= 5.022, experimental => 're_strict';
  34         103  
  34         407  
15 34     34   3269 no if "$]" >= 5.031009, feature => 'indirect';
  34         148  
  34         327  
16 34     34   1868 no if "$]" >= 5.033001, feature => 'multidimensional';
  34         87  
  34         295  
17 34     34   1629 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  34         82  
  34         199  
18 34     34   1350 use Mojo::URL;
  34         94  
  34         551  
19 34     34   1477 use Carp 'croak';
  34         94  
  34         2564  
20 34     34   247 use List::Util 1.29 'pairs';
  34         601  
  34         2573  
21 34     34   272 use Ref::Util 0.100 'is_plain_hashref';
  34         810  
  34         2146  
22 34     34   265 use Safe::Isa 1.000008;
  34         752  
  34         4465  
23 34     34   270 use MooX::TypeTiny;
  34         132  
  34         307  
24 34     34   28726 use MooX::HandlesVia;
  34         90  
  34         230  
25 34     34   4231 use Types::Standard 1.016003 qw(InstanceOf HashRef Str Dict ArrayRef Enum ClassName Undef Slurpy);
  34         737  
  34         247  
26 34     34   128380 use namespace::clean;
  34         121  
  34         290  
27              
28             extends 'Mojo::JSON::Pointer';
29              
30             has schema => (
31             is => 'ro',
32             required => 1,
33             );
34              
35             has canonical_uri => (
36             is => 'rwp',
37             isa => (InstanceOf['Mojo::URL'])->where(q{not defined $_->fragment}),
38             lazy => 1,
39             default => sub { Mojo::URL->new },
40             coerce => sub { $_[0]->$_isa('Mojo::URL') ? $_[0] : Mojo::URL->new($_[0]) },
41             );
42              
43             has metaschema_uri => (
44             is => 'rwp',
45             isa => InstanceOf['Mojo::URL'],
46             coerce => sub { $_[0]->$_isa('Mojo::URL') ? $_[0] : Mojo::URL->new($_[0]) },
47             );
48              
49             has evaluator => (
50             is => 'rwp',
51             isa => InstanceOf['JSON::Schema::Modern'],
52             weak_ref => 1,
53             );
54              
55             # "A JSON Schema resource is a schema which is canonically identified by an absolute URI."
56             # https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.4.3.5
57             has resource_index => (
58             is => 'bare',
59             isa => HashRef[my $resource_type = Dict[
60             canonical_uri => InstanceOf['Mojo::URL'],
61             path => Str, # always a JSON pointer, relative to the document root
62             specification_version => Str, # not an Enum due to module load ordering
63             # the vocabularies used when evaluating instance data against schema
64             vocabularies => ArrayRef[ClassName->where(q{$_->DOES('JSON::Schema::Modern::Vocabulary')})],
65             configs => HashRef,
66             Slurpy[HashRef[Undef]], # no other fields allowed
67             ]],
68             handles_via => 'Hash',
69             handles => {
70             resource_index => 'elements',
71             resource_pairs => 'kv',
72             _add_resources => 'set',
73             _get_resource => 'get',
74             _remove_resource => 'delete',
75             _canonical_resources => 'values',
76             },
77             init_arg => undef,
78             lazy => 1,
79             default => sub { {} },
80             );
81              
82             has _path_to_resource => (
83             is => 'bare',
84             isa => HashRef[$resource_type],
85             handles_via => 'Hash',
86             handles => {
87             path_to_resource => 'get',
88             },
89             init_arg => undef,
90             lazy => 1,
91             default => sub { +{ map +($_->{path} => $_), shift->_canonical_resources } },
92             );
93              
94             # for internal use only
95             has _serialized_schema => (
96             is => 'rw',
97             isa => Str,
98             init_arg => undef,
99             );
100              
101             has errors => (
102             is => 'bare',
103             handles_via => 'Array',
104             handles => {
105             errors => 'elements',
106             has_errors => 'count',
107             },
108             writer => '_set_errors',
109             isa => ArrayRef[InstanceOf['JSON::Schema::Modern::Error']],
110             lazy => 1,
111             default => sub { [] },
112             );
113              
114             around _add_resources => sub {
115             my $orig = shift;
116             my $self = shift;
117              
118             foreach my $pair (pairs @_) {
119             my ($key, $value) = @$pair;
120              
121             $resource_type->($value); # check type of hash value against Dict
122              
123             if (my $existing = $self->_get_resource($key)) {
124             croak 'uri "'.$key.'" conflicts with an existing schema resource'
125             if $existing->{path} ne $value->{path}
126             or $existing->{canonical_uri} ne $value->{canonical_uri}
127             or $existing->{specification_version} ne $value->{specification_version};
128             }
129              
130             # this will never happen, if we parsed $id correctly
131             croak sprintf('a resource canonical uri cannot contain a plain-name fragment (%s)', $value->{canonical_uri})
132             if ($value->{canonical_uri}->fragment // '') =~ m{^[^/]};
133              
134             $self->$orig($key, $value);
135             }
136             };
137              
138             # shims for Mojo::JSON::Pointer
139 4378     4378 1 44318 sub data { shift->schema(@_) }
140 13633     13633 0 1125176 sub FOREIGNBUILDARGS { () }
141              
142             # for JSON serializers
143 0     0 1 0 sub TO_JSON { shift->schema }
144              
145 13632     13632 0 1033423 sub BUILD ($self, $args) {
  13632         24390  
  13632         22790  
  13632         20056  
146 13632         224873 my $original_uri = $self->canonical_uri->clone;
147 13632   66     763148 my $state = $self->traverse($self->evaluator // JSON::Schema::Modern->new);
148              
149             # if the schema identified a canonical uri for itself, it overrides the initial value
150 13632 100       59260 $self->_set_canonical_uri($state->{initial_schema_uri}) if $state->{initial_schema_uri} ne $original_uri;
151              
152 13632 100       2970712 if ($state->{errors}->@*) {
153 121         373 foreach my $error ($state->{errors}->@*) {
154 147 50       4157 $error->mode('traverse') if not defined $error->mode;
155             }
156              
157 121         6133 $self->_set_errors($state->{errors});
158 121         3006 return;
159             }
160              
161             # make sure the root schema is always indexed against *something*.
162             $self->_add_resources($original_uri => {
163             path => '',
164             canonical_uri => $self->canonical_uri,
165             specification_version => $state->{spec_version},
166             vocabularies => $state->{vocabularies},
167             configs => $state->{configs},
168             })
169 13511 100 100     37238 if (not "$original_uri" and $original_uri eq $self->canonical_uri)
      100        
170             or "$original_uri";
171              
172 13511         4779927 $self->_add_resources($state->{identifiers}->@*);
173             }
174              
175 13632     13632 0 25863 sub traverse ($self, $evaluator) {
  13632         21979  
  13632         20618  
  13632         20176  
176             die 'wrong class - use JSON::Schema::Modern::Document::OpenAPI instead'
177 13632 50 66     77593 if is_plain_hashref($self->schema) and exists $self->schema->{openapi};
178              
179 13632 100       260693 my $state = $evaluator->traverse($self->schema,
180             {
181             initial_schema_uri => $self->canonical_uri->clone,
182             $self->metaschema_uri ? ( metaschema_uri => $self->metaschema_uri) : (),
183             }
184             );
185              
186 13632 100       84540 return $state if $state->{errors}->@*;
187              
188             # we don't store the metaschema_uri in $state nor in resource_index, but we can figure it out
189             # easily enough.
190             my $metaschema_uri = (is_plain_hashref($self->schema) ? $self->schema->{'$schema'} : undef)
191 13511 100 100     103588 // $self->metaschema_uri // $evaluator->METASCHEMA_URIS->{$state->{spec_version}};
      66        
192              
193 13511 100 100     362100 $self->_set_metaschema_uri($metaschema_uri) if $metaschema_uri ne ($self->metaschema_uri//'');
194              
195 13511         1740752 return $state;
196             }
197              
198 1     1 1 3 sub validate ($self) {
  1         2  
  1         2  
199 1   33     7 my $js = $self->$_call_if_can('evaluator') // JSON::Schema::Modern->new;
200              
201 1         34 return $js->evaluate($self->schema, $self->metaschema_uri);
202             }
203              
204             1;
205              
206             __END__
207              
208             =pod
209              
210             =encoding UTF-8
211              
212             =for stopwords subschema
213              
214             =head1 NAME
215              
216             JSON::Schema::Modern::Document - One JSON Schema document
217              
218             =head1 VERSION
219              
220             version 0.570
221              
222             =head1 SYNOPSIS
223              
224             use JSON::Schema::Modern::Document;
225              
226             my $document = JSON::Schema::Modern::Document->new(
227             canonical_uri => 'https://example.com/v1/schema',
228             metaschema_uri => 'https://example.com/my/custom/metaschema',
229             schema => $schema,
230             );
231             my $foo_definition = $document->get('/$defs/foo');
232             my %resource_index = $document->resource_index;
233              
234             my sanity_check = $document->validate;
235              
236             =head1 DESCRIPTION
237              
238             This class represents one JSON Schema document, to be used by L<JSON::Schema::Modern>.
239              
240             =head1 ATTRIBUTES
241              
242             =head2 schema
243              
244             The actual raw data representing the schema.
245              
246             =head2 canonical_uri
247              
248             When passed in during construction, this represents the initial URI by which the document should
249             be known. It is overwritten with the root schema's C<$id> property when one exists, and as such
250             can be considered the canonical URI for the document as a whole.
251              
252             =head2 metaschema_uri
253              
254             =for stopwords metaschema schemas
255              
256             Sets the metaschema that is used to describe the document (or more specifically, any JSON Schemas
257             contained within the document), which determines the
258             specification version and vocabularies used during evaluation. Does not override any
259             C<$schema> keyword actually present in the schema document.
260              
261             =head2 evaluator
262              
263             A L<JSON::Schema::Modern> object. Optional, unless custom metaschemas are used.
264              
265             =head2 resource_index
266              
267             An index of URIs to subschemas (JSON pointer to reach the location, and the canonical URI of that
268             location) for all identifiable subschemas found in the document. An entry for URI C<''> is added
269             only when no other suitable identifier can be found for the root schema.
270              
271             This attribute should only be used by L<JSON::Schema::Modern> and not intended for use
272             externally (you should use the public accessors in L<JSON::Schema::Modern> instead).
273              
274             When called as a method, returns the flattened list of tuples (path, uri). You can also use
275             C<resource_pairs> which returns a list of tuples as arrayrefs.
276              
277             =head2 canonical_uri_index
278              
279             An index of JSON pointers (from the document root) to canonical URIs. This is the inversion of
280             L</resource_index> and is constructed as that is built up.
281              
282             =head2 errors
283              
284             A list of L<JSON::Schema::Modern::Error> objects that resulted when the schema document was
285             originally parsed. (If a syntax error occurred, usually there will be just one error, as parse
286             errors halt the parsing process.) Documents with errors cannot be evaluated.
287              
288             =head1 METHODS
289              
290             =for Pod::Coverage FOREIGNBUILDARGS BUILDARGS BUILD traverse
291              
292             =head2 path_to_canonical_uri
293              
294             =for stopwords fragmentless
295              
296             Given a JSON pointer (a path) within this document, returns the canonical URI corresponding to that location.
297             Only fragmentless URIs can be looked up in this manner, so it is only suitable for finding the
298             canonical URI corresponding to a subschema known to have an C<$id> keyword.
299              
300             =head2 contains
301              
302             Check if L</"schema"> contains a value that can be identified with the given JSON Pointer.
303             See L<Mojo::JSON::Pointer/contains>.
304              
305             =head2 get
306              
307             Extract value from L</"schema"> identified by the given JSON Pointer.
308             See L<Mojo::JSON::Pointer/get>.
309              
310             =head2 validate
311              
312             Evaluates the document against its metaschema. See L<JSON::Schema::Modern/evaluate>.
313             For regular JSON Schemas this is redundant with creating the document in the first place (which also
314             includes a validation check), but for some subclasses of this class, additional things might be
315             checked that are not caught by document creation.
316              
317             =head2 TO_JSON
318              
319             Returns a data structure suitable for serialization. See L</schema>.
320              
321             =head1 SEE ALSO
322              
323             =over 4
324              
325             =item *
326              
327             L<JSON::Schema::Modern>
328              
329             =item *
330              
331             L<Mojo::JSON::Pointer>
332              
333             =back
334              
335             =for stopwords OpenAPI
336              
337             =head1 SUPPORT
338              
339             Bugs may be submitted through L<https://github.com/karenetheridge/JSON-Schema-Modern/issues>.
340              
341             I am also usually active on irc, as 'ether' at C<irc.perl.org> and C<irc.libera.chat>.
342              
343             You can also find me on the L<JSON Schema Slack server|https://json-schema.slack.com> and L<OpenAPI Slack
344             server|https://open-api.slack.com>, which are also great resources for finding help.
345              
346             =head1 AUTHOR
347              
348             Karen Etheridge <ether@cpan.org>
349              
350             =head1 COPYRIGHT AND LICENCE
351              
352             This software is copyright (c) 2020 by Karen Etheridge.
353              
354             This is free software; you can redistribute it and/or modify it under
355             the same terms as the Perl 5 programming language system itself.
356              
357             =cut