|
| 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