File Coverage

blib/lib/JSON/Schema/Modern/Document.pm
Criterion Covered Total %
statement 95 96 98.9
branch 16 18 88.8
condition 21 27 77.7
subroutine 31 32 96.8
pod 5 11 45.4
total 168 184 91.3


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