Skip to content

Commit 75b4b95

Browse files
store schemas in separate Document objects
1 parent ad54a57 commit 75b4b95

File tree

4 files changed

+534
-165
lines changed

4 files changed

+534
-165
lines changed

lib/JSON/Schema/Draft201909.pm

+20-58
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package JSON::Schema::Draft201909;
88
our $VERSION = '0.004';
99

1010
no if "$]" >= 5.031009, feature => 'indirect';
11-
use feature 'current_sub';
1211
use JSON::MaybeXS 1.004001 'is_bool';
1312
use Syntax::Keyword::Try 0.11;
1413
use Carp 'croak';
@@ -19,9 +18,10 @@ use Safe::Isa;
1918
use Moo;
2019
use MooX::TypeTiny 0.002002;
2120
use MooX::HandlesVia;
22-
use Types::Standard 1.010002 qw(Bool Int HasMethods Enum InstanceOf HashRef Dict);
21+
use Types::Standard 1.010002 qw(Bool Int Str HasMethods Enum InstanceOf HashRef Dict);
2322
use JSON::Schema::Draft201909::Error;
2423
use JSON::Schema::Draft201909::Result;
24+
use JSON::Schema::Draft201909::Document;
2525
use namespace::clean;
2626

2727
has output_format => (
@@ -69,7 +69,16 @@ sub evaluate_json_string {
6969
sub evaluate {
7070
my ($self, $data, $schema) = @_;
7171

72-
$self->_find_all_identifiers($schema);
72+
# TODO: move to $self->add_schema($schema)
73+
my $document = JSON::Schema::Draft201909::Document->new(
74+
# TODO canonical_uri => $self->base_uri,
75+
schema => $schema,
76+
);
77+
78+
$self->_add_resources(
79+
map +( $_->[0] => +{ %{$_->[1]}, document => $document } ),
80+
$document->_resource_pairs
81+
);
7382

7483
my $state = {
7584
base_uri => Mojo::URL->new, # TODO: will be set by a global attribute
@@ -212,16 +221,16 @@ sub _fetch_and_eval_ref_uri {
212221

213222
my $fragment = $uri->fragment // '';
214223
my ($subschema, $canonical_uri);
215-
# TODO: this will get less ugly when we move to actual document objects
216224
if (not length($fragment) or $fragment =~ m{^/}) {
217225
my $base = $uri->clone->fragment(undef);
218-
my $document = Mojo::JSON::Pointer->new(($self->_get_resource($base) // {})->{ref});
219-
$subschema = $document->get($fragment);
220-
$canonical_uri = $uri;
226+
if (my $resource = $self->_get_resource($base)) {
227+
$subschema = $resource->{document}->get($resource->{path}.$fragment);
228+
$canonical_uri = $uri;
229+
}
221230
}
222231
else {
223232
if (my $resource = $self->_get_resource($uri)) {
224-
$subschema = $resource->{ref};
233+
$subschema = $resource->{document}->get($resource->{path});
225234
$canonical_uri = $resource->{canonical_uri}->clone; # this is *not* the anchor-containing URI
226235
}
227236
}
@@ -936,59 +945,12 @@ sub _is_elements_unique {
936945
return 1;
937946
}
938947

939-
sub _traverse_for_identifiers {
940-
my ($data, $canonical_uri) = @_;
941-
my $uri_fragment = $canonical_uri->fragment // '';
942-
my %identifiers;
943-
if (ref $data eq 'ARRAY') {
944-
return map
945-
__SUB__->($data->[$_], $canonical_uri->clone->fragment($uri_fragment.'/'.$_)),
946-
0 .. $#{$data};
947-
}
948-
elsif (ref $data eq 'HASH') {
949-
if (exists $data->{'$id'} and _is_type(undef, 'string', $data->{'$id'})) {
950-
$canonical_uri = Mojo::URL->new($data->{'$id'})->base($canonical_uri)->to_abs;
951-
# this might not be a real $id... wait for it to be encountered at runtime before dying
952-
if (not length $canonical_uri->fragment) {
953-
$canonical_uri->fragment(undef);
954-
$identifiers{$canonical_uri} = { ref => $data, canonical_uri => $canonical_uri };
955-
};
956-
}
957-
if (exists $data->{'$anchor'} and _is_type(undef, 'string', $data->{'$anchor'})) {
958-
# we cannot change the canonical uri, or we won't be able to properly identify
959-
# paths within this resource
960-
my $uri = Mojo::URL->new->base($canonical_uri)->to_abs->fragment($data->{'$anchor'});
961-
$identifiers{$uri} = { ref => $data, canonical_uri => $canonical_uri };
962-
}
963-
964-
return
965-
%identifiers,
966-
map __SUB__->($data->{$_}, $canonical_uri->clone->fragment($uri_fragment.'/'.$_)), keys %$data;
967-
}
968-
969-
return ();
970-
}
971-
972-
# traverse a schema document, find all identifiers and add them to the resource index.
973-
# internal only and subject to change!
974-
sub _find_all_identifiers {
975-
my ($self, $schema) = @_;
976-
977-
my $base_uri = Mojo::URL->new; # TODO: $self->base_uri->clone
978-
my %identifiers = _traverse_for_identifiers($schema, $base_uri);
979-
980-
$identifiers{''} = { ref => $schema, canonical_uri => $base_uri }
981-
if not "$base_uri" and ref $schema eq 'HASH' and not exists $schema->{'$id'};
982-
983-
$self->_add_resources(%identifiers);
984-
}
985-
986948
has _resource_index => (
987949
is => 'bare',
988950
isa => HashRef[Dict[
989-
# see JSON::MaybeXS::is_bool
990-
ref => InstanceOf[qw(JSON::XS::Boolean Cpanel::JSON::XS::Boolean JSON::PP::Boolean)]|HashRef,
991951
canonical_uri => InstanceOf['Mojo::URL'],
952+
path => Str,
953+
document => InstanceOf['JSON::Schema::Draft201909::Document'],
992954
]],
993955
handles_via => 'Hash',
994956
handles => {
@@ -1010,7 +972,7 @@ before _add_resources => sub {
1010972
# we allow overwriting canonical_uri = '' to allow for ad hoc evaluation of
1011973
# schemas that lack all identifiers altogether
1012974
if ($key ne '' and $existing->{canonical_uri} ne '')
1013-
and $existing->{ref} != $value->{ref}
975+
and $existing->{path} ne $value->{path}
1014976
or $existing->{canonical_uri} ne $value->{canonical_uri};
1015977
}
1016978

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
use strict;
2+
use warnings;
3+
package JSON::Schema::Draft201909::Document;
4+
# vim: set ts=8 sts=2 sw=2 tw=100 et :
5+
# ABSTRACT: One JSON Schema document
6+
7+
our $VERSION = '0.003';
8+
9+
no if "$]" >= 5.031009, feature => 'indirect';
10+
use feature 'current_sub';
11+
use Mojo::URL;
12+
use Carp 'croak';
13+
use JSON::MaybeXS 1.004001 'is_bool';
14+
use List::Util 1.29 'pairs';
15+
use Moo;
16+
use MooX::TypeTiny;
17+
use MooX::HandlesVia;
18+
use Types::Standard qw(InstanceOf HashRef Str Dict HasMethods);
19+
use namespace::clean;
20+
21+
extends 'Mojo::JSON::Pointer';
22+
23+
has schema => (
24+
is => 'ro',
25+
required => 1,
26+
);
27+
28+
has canonical_uri => (
29+
is => 'rwp',
30+
isa => InstanceOf['Mojo::URL'],
31+
lazy => 1,
32+
default => sub { Mojo::URL->new },
33+
);
34+
35+
has resource_index => (
36+
is => 'bare',
37+
isa => HashRef[Dict[
38+
canonical_uri => InstanceOf['Mojo::URL'], # always fragmentless
39+
path => Str, # always a json pointer
40+
]],
41+
handles_via => 'Hash',
42+
handles => {
43+
resource_index => 'elements',
44+
_add_resources => 'set',
45+
_get_resource => 'get',
46+
_remove_resource => 'delete',
47+
_resource_pairs => 'kv',
48+
},
49+
init_arg => undef,
50+
lazy => 1,
51+
default => sub { {} },
52+
);
53+
54+
before _add_resources => sub {
55+
my $self = shift;
56+
foreach my $pair (pairs @_) {
57+
my ($key, $value) = @$pair;
58+
if (my $existing = $self->_get_resource($key)) {
59+
croak 'a schema resource is already indexed with uri "'.$key.'"'
60+
if $existing->{path} != $value->{path}
61+
or $existing->{canonical_uri} ne $value->{canonical_uri};
62+
}
63+
64+
croak sprintf('canonical_uri cannot contain a plain-name fragment (%s)', $value->{canonical_uri})
65+
if ($value->{canonical_uri}->fragment // '') =~ m{^[^/]};
66+
}
67+
};
68+
69+
# shims for Mojo::JSON::Pointer
70+
sub data { goto \&schema }
71+
sub FOREIGNBUILDARGS { () }
72+
73+
sub BUILD {
74+
my $self = shift;
75+
76+
croak 'canonical_uri cannot contain a fragment' if defined $self->canonical_uri->fragment;
77+
78+
my $canonical_uri = $self->canonical_uri->clone;
79+
my $schema = $self->data;
80+
my %identifiers = _traverse_for_identifiers($schema, $canonical_uri);
81+
82+
if (ref $self->schema eq 'HASH') {
83+
my $id = $self->get('/$id');
84+
$self->_set_canonical_uri(Mojo::URL->new($id)) if defined $id and $id ne $self->canonical_uri;
85+
}
86+
87+
# make sure the root schema is always indexed against *something*.
88+
$identifiers{$canonical_uri} = { path => '', canonical_uri => $self->canonical_uri }
89+
if (not "$canonical_uri" and $canonical_uri eq $self->canonical_uri)
90+
or ("$canonical_uri" and not exists $identifiers{$canonical_uri});
91+
92+
$self->_add_resources(%identifiers);
93+
}
94+
95+
sub _traverse_for_identifiers {
96+
my ($data, $canonical_uri) = @_;
97+
my $uri_fragment = $canonical_uri->fragment // '';
98+
my %identifiers;
99+
if (ref $data eq 'ARRAY') {
100+
return map
101+
__SUB__->($data->[$_], $canonical_uri->clone->fragment($uri_fragment.'/'.$_)),
102+
0 .. $#{$data};
103+
}
104+
elsif (ref $data eq 'HASH') {
105+
if (exists $data->{'$id'} and _is_type(undef, 'string', $data->{'$id'})) {
106+
$canonical_uri = Mojo::URL->new($data->{'$id'})->base($canonical_uri)->to_abs;
107+
# this might not be a real $id... wait for it to be encountered at runtime before dying
108+
if (not length $canonical_uri->fragment) {
109+
$canonical_uri->fragment(undef);
110+
$identifiers{$canonical_uri} = { path => $uri_fragment, canonical_uri => $canonical_uri };
111+
};
112+
}
113+
if (exists $data->{'$anchor'} and _is_type(undef, 'string', $data->{'$anchor'})) {
114+
# we cannot change the canonical uri, or we won't be able to properly identify
115+
# paths within this resource
116+
my $uri = Mojo::URL->new->base($canonical_uri)->to_abs->fragment($data->{'$anchor'});
117+
$identifiers{$uri} = { path => $uri_fragment, canonical_uri => $canonical_uri };
118+
}
119+
120+
return
121+
%identifiers,
122+
map __SUB__->($data->{$_}, $canonical_uri->clone->fragment($uri_fragment.'/'.$_)), keys %$data;
123+
}
124+
125+
return ();
126+
}
127+
128+
# copied from JSON::Schema::Draft201909 (ugh)
129+
sub _is_type {
130+
my (undef, $type, $value) = @_;
131+
132+
if ($type eq 'null') {
133+
return !(defined $value);
134+
}
135+
if ($type eq 'boolean') {
136+
return is_bool($value);
137+
}
138+
if ($type eq 'object') {
139+
return ref $value eq 'HASH';
140+
}
141+
if ($type eq 'array') {
142+
return ref $value eq 'ARRAY';
143+
}
144+
145+
if ($type eq 'string' or $type eq 'number' or $type eq 'integer') {
146+
return 0 if not defined $value or ref $value;
147+
my $flags = B::svref_2object(\$value)->FLAGS;
148+
149+
if ($type eq 'string') {
150+
return $flags & B::SVf_POK && !($flags & (B::SVf_IOK | B::SVf_NOK));
151+
}
152+
153+
if ($type eq 'number') {
154+
return !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK));
155+
}
156+
157+
if ($type eq 'integer') {
158+
return !($flags & B::SVf_POK) && ($flags & (B::SVf_IOK | B::SVf_NOK))
159+
&& int($value) == $value;
160+
}
161+
}
162+
163+
croak sprintf('unknown type "%s"', $type);
164+
}
165+
166+
1;
167+
__END__
168+
169+
=pod
170+
171+
=for :header
172+
=for stopwords subschema
173+
174+
=head1 SYNOPSIS
175+
176+
use JSON::Schema::Draft201909::Document;
177+
178+
my $document = JSON::Schema::Draft201909::Document->new(
179+
canonical_uri => 'https://mycorp.com/v1/schema',
180+
schema => $schema,
181+
);
182+
my $foo_definition = $document->get('/$defs/foo');
183+
my %resource_index = $document->resource_index;
184+
185+
=head1 DESCRIPTION
186+
187+
This class represents one JSON Schema document, to be used by L<JSON::Schema::Draft201909>.
188+
189+
=head1 ATTRIBUTES
190+
191+
=head2 schema
192+
193+
=head2 data
194+
195+
The actual raw data representing the schema.
196+
197+
=head2 canonical_uri
198+
199+
When passed in during construction, this represents the initial URI by which the document should
200+
be known. It is overwritten with the root schema's C<$id> property when one exists, and as such
201+
can be considered the canonical URI for the document as a whole.
202+
203+
=head2 resource_index
204+
205+
An index of URIs to subschemas (json path to reach the location, and the canonical uri of that
206+
location) for all identifiable subschemas found in the document. An entry for URI C<''> is added
207+
only when no other suitable identifier can be found for the root schema.
208+
209+
This attribute should only be used by L<JSON::Schema::Draft201909> and not intended for use
210+
externally (you should use the public accessors in L<JSON::Schema::Draft201909> instead).
211+
212+
=head1 METHODS
213+
214+
=for Pod::Coverage BUILD FOREIGNBUILDARGS
215+
216+
=head2 contains
217+
218+
See L<Mojo::JSON::Pointer/contains>.
219+
220+
=head2 get
221+
222+
See L<Mojo::JSON::Pointer/get>.
223+
224+
=cut

0 commit comments

Comments
 (0)