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   732 use strict;
  34         85  
  34         1293  
2 34     34   217 use warnings;
  34         112  
  34         1809  
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.571';
8              
9 34     34   750 use 5.020;
  34         136  
10 34     34   216 use Moo;
  34         102  
  34         262  
11 34     34   13866 use strictures 2;
  34         301  
  34         1983  
12 34     34   7162 use stable 0.031 'postderef';
  34         693  
  34         268  
13 34     34   5936 use experimental 'signatures';
  34         107  
  34         179  
14 34     34   3005 use if "$]" >= 5.022, experimental => 're_strict';
  34         109  
  34         399  
15 34     34   3504 no if "$]" >= 5.031009, feature => 'indirect';
  34         105  
  34         344  
16 34     34   1888 no if "$]" >= 5.033001, feature => 'multidimensional';
  34         142  
  34         250  
17 34     34   1788 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  34         89  
  34         248  
18 34     34   1467 use Mojo::URL;
  34         108  
  34         643  
19 34     34   1549 use Carp 'croak';
  34         97  
  34         2723  
20 34     34   262 use List::Util 1.29 'pairs';
  34         649  
  34         2813  
21 34     34   310 use Ref::Util 0.100 'is_plain_hashref';
  34         878  
  34         2031  
22 34     34   264 use Safe::Isa 1.000008;
  34         710  
  34         4562  
23 34     34   277 use MooX::TypeTiny;
  34         81  
  34         353  
24 34     34   29867 use MooX::HandlesVia;
  34         95  
  34         289  
25 34     34   4198 use Types::Standard 1.016003 qw(InstanceOf HashRef Str Dict ArrayRef Enum ClassName Undef Slurpy);
  34         750  
  34         280  
26 34     34   130760 use namespace::clean;
  34         88  
  34         307  
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 4474     4474 1 45807 sub data { shift->schema(@_) }
140 13785     13785 0 1180323 sub FOREIGNBUILDARGS { () }
141              
142             # for JSON serializers
143 0     0 1 0 sub TO_JSON { shift->schema }
144              
145 13784     13784 0 1067206 sub BUILD ($self, $args) {
  13784         24786  
  13784         22088  
  13784         22022  
146 13784         232931 my $original_uri = $self->canonical_uri->clone;
147 13784   66     787369 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 13784 100       61523 $self->_set_canonical_uri($state->{initial_schema_uri}) if $state->{initial_schema_uri} ne $original_uri;
151              
152 13784 100       3082826 if ($state->{errors}->@*) {
153 121         495 foreach my $error ($state->{errors}->@*) {
154 147 50       4637 $error->mode('traverse') if not defined $error->mode;
155             }
156              
157 121         6558 $self->_set_errors($state->{errors});
158 121         3154 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 13663 100 100     40157 if (not "$original_uri" and $original_uri eq $self->canonical_uri)
      100        
170             or "$original_uri";
171              
172 13663         4968294 $self->_add_resources($state->{identifiers}->@*);
173             }
174              
175 13784     13784 0 26311 sub traverse ($self, $evaluator) {
  13784         22817  
  13784         20627  
  13784         19595  
176             die 'wrong class - use JSON::Schema::Modern::Document::OpenAPI instead'
177 13784 50 66     82426 if is_plain_hashref($self->schema) and exists $self->schema->{openapi};
178              
179 13784 100       264843 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 13784 100       91347 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 13663 100 100     106488 // $self->metaschema_uri // $evaluator->METASCHEMA_URIS->{$state->{spec_version}};
      66        
192              
193 13663 100 100     376630 $self->_set_metaschema_uri($metaschema_uri) if $metaschema_uri ne ($self->metaschema_uri//'');
194              
195 13663         1808313 return $state;
196             }
197              
198 1     1 1 2 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         31 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.571
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