File: Store.pm

package info (click to toggle)
libjson-validator-perl 5.14%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,160 kB
  • sloc: perl: 3,015; makefile: 14
file content (324 lines) | stat: -rw-r--r-- 9,683 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
package JSON::Validator::Store;
use Mojo::Base -base;

use Mojo::Exception;
use Mojo::File qw(path);
use Mojo::JSON;
use Mojo::JSON::Pointer;
use Mojo::UserAgent;
use Mojo::Util qw(url_unescape);
use JSON::Validator::Schema;
use JSON::Validator::URI qw(uri);
use JSON::Validator::Util qw(data_section str2data);
use Scalar::Util qw(blessed);

use constant DEBUG         => $ENV{JSON_VALIDATOR_DEBUG} && 1;
use constant BUNDLED_PATH  => path(path(__FILE__)->dirname, 'cache')->to_string;
use constant CASE_TOLERANT => File::Spec->case_tolerant;

die $@ unless eval q(package JSON::Validator::Exception; use Mojo::Base 'Mojo::Exception'; 1);

has cache_paths => sub { [split(/:/, $ENV{JSON_VALIDATOR_CACHE_PATH} || ''), BUNDLED_PATH] };
has schemas     => sub { +{} };

has ua => sub {
  my $ua = Mojo::UserAgent->new;
  $ua->proxy->detect;
  return $ua->max_redirects(3);
};

sub add {
  my ($self, $id, $schema) = @_;
  $id =~ s!(.)#$!$1!;
  $self->schemas->{$id} = $schema;
  return $id;
}

sub exists {
  my ($self, $id) = @_;
  return undef unless defined $id;
  $id =~ s!(.)#$!$1!;
  return $self->schemas->{$id} && $id;
}

sub get {
  my ($self, $id) = @_;
  return undef unless defined $id;
  $id =~ s!(.)#$!$1!;
  return $self->schemas->{$id};
}

sub load {
  return
       $_[0]->_load_from_url($_[1])
    || $_[0]->_load_from_data($_[1])
    || $_[0]->_load_from_text($_[1])
    || $_[0]->_load_from_file($_[1])
    || $_[0]->_load_from_app($_[1])
    || $_[0]->get($_[1])
    || _raise(qq(Unable to load schema "$_[1]".));
}

sub resolve {
  my ($self, $ref, $curr) = @_;
  $curr //= {base_url => ''};

  my ($base_url, $fragment) = split '#', $ref;
  my $abs_url = uri($base_url)->fragment($fragment);
  $abs_url  = uri $abs_url, $curr->{base_url} if $curr->{base_url} and !$abs_url->is_abs;
  $fragment = '' unless defined $fragment;
  $base_url ||= $curr->{base_url} || '';

  warn "[JSON::Validator] Resolve curr: ref=$ref,@{[map qq($_=$curr->{$_}), sort keys %$curr]}\n" if DEBUG;

  my $state = {base_url => $base_url, fragment => $fragment, source => 'unknown'};
  if (defined(my $schema = $self->schemas->{$abs_url})) {
    @$state{qw(base_url id root schema source)} = ($abs_url, $abs_url, $schema, $schema, 'schema/abs_url');
  }
  elsif (defined(my $root = $self->schemas->{$base_url})) {
    @$state{qw(base_url id root source)} = ($base_url, $base_url, $root, 'schema/base_url');
  }
  elsif ($base_url) {
    $base_url = uri $base_url, $curr->{base_url} if $curr->{base_url};
    my $id = $self->load($base_url);
    @$state{qw(base_url id root source)} = ($id, $id, $self->get($id), 'load');
    $state->{root} = $self->get($id);
  }
  else {
    @$state{qw(id root source)} = ('', $curr->{root}, 'root');
  }

  $fragment =~ s!%2f!~1!;    # /
  $fragment =~ s!%7e!~0!;    # ~
  $fragment = url_unescape $fragment;
  $state->{schema} //= length $fragment ? Mojo::JSON::Pointer->new($state->{root})->get($fragment) : $state->{root};
  _raise(qq[Unable to resolve "$ref" from "$state->{base_url}". ($state->{source})]) unless defined $state->{schema};

  $state->{$_} //= $curr->{$_} for keys %$curr;    # pass on original information
  warn "[JSON::Validator] Resolve state: @{[map qq($_=$state->{$_}), sort keys %$state]}\n" if DEBUG;
  return $state;
}

sub _add {
  my ($self, $id, $schema) = @_;
  $id = $self->add($id => $schema);

  if (ref $schema eq 'HASH') {
    return
        $schema->{'$id'} ? $self->add($schema->{'$id'} => $schema)
      : $schema->{id}    ? $self->add($schema->{id} => $schema)
      :                    $id;
  }

  return $id;
}

sub _load_from_app {
  return undef unless $_[1] =~ m!^/!;

  my ($self, $url) = @_;
  my $id;
  return undef unless $self->ua->server->app;
  return undef if blessed $url and !$url->can('scheme');
  return $id if $id = $self->exists($url);

  my $tx  = $self->ua->get($url);
  my $err = $tx->error && $tx->error->{message};
  _raise("GET $url: $err")                      if $err;
  warn "[JSON::Validator] Load from app $url\n" if DEBUG;
  return $self->_add($url => str2data $tx->res->body);
}

sub _load_from_data {
  return undef unless $_[1] =~ m!^data://([^/]*)/(.*)!;

  my ($self, $url) = @_;
  my $id;
  return $id if $id = $self->exists($url);

  my ($class, $file) = ($1, $2);    # data://([^/]*)/(.*)
  my $text = data_section $class, $file, {encoding => 'UTF-8'};
  _raise("Could not find $url") unless $text;
  warn "[JSON::Validator] Load from data $file in $class\n" if DEBUG;
  return $self->_add($url => str2data $text);
}

sub _load_from_file {
  my ($self, $file) = @_;

  $file =~ s!^file://!!;
  $file =~ s!#$!!;
  $file = path(split '/', url_unescape $file);
  return undef unless -e $file;

  $file = $file->realpath;
  my $id = uri()->new->scheme('file')->host('')->path(CASE_TOLERANT ? lc $file : "$file");
  warn "[JSON::Validator] Load from file $file\n" if DEBUG;
  return $self->exists($id) || $self->_add($id => str2data $file->slurp);
}

sub _load_from_text {
  my ($self, $text) = @_;
  my $is_scalar_ref = ref $text eq 'SCALAR';
  return undef unless $is_scalar_ref or $text =~ m!^\s*(?:---|\{)!s;

  my $id = uri->from_data($is_scalar_ref ? $$text : $text);
  warn "[JSON::Validator] Load from text $id\n" if DEBUG;
  return $self->exists($id) || $self->_add($id => str2data $is_scalar_ref ? $$text : $text);
}

sub _load_from_url {
  return undef unless $_[1] =~ m!^https?://!;

  my ($self, $url) = @_;
  my $id;
  return $id if $id = $self->exists($url);

  $url = uri($url)->fragment(undef);
  return $id if $id = $self->exists($url);

  my $cache_path = $self->cache_paths->[0];
  my $cache_file = Mojo::Util::md5_sum("$url");
  for (@{$self->cache_paths}) {
    my $path = path $_, $cache_file;
    warn "[JSON::Validator] Load from cache $path\n"  if DEBUG and -r $path;
    return $self->_add($url => str2data $path->slurp) if -r $path;
  }

  my $tx  = $self->ua->get($url);
  my $err = $tx->error && $tx->error->{message};
  _raise("GET $url: $err") if $err;

  if ($cache_path and $cache_path ne BUNDLED_PATH and -w $cache_path) {
    $cache_file = path $cache_path, $cache_file;
    $cache_file->spurt($tx->res->body);
  }

  warn "[JSON::Validator] Load from URL $url\n" if DEBUG;
  return $self->_add($url => str2data $tx->res->body);
}

sub _raise { die JSON::Validator::Exception->new(@_)->trace }

1;

=encoding utf8

=head1 NAME

JSON::Validator::Store - Load and caching JSON schemas

=head1 SYNOPSIS

  use JSON::Validator;
  my $jv = JSON::Validator->new;
  $jv->store->add("urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" => {...});
  $jv->store->load("http://api.example.com/my/schema.json");

=head1 DESCRIPTION

L<JSON::Validator::Store> is a class for loading and caching JSON-Schemas.

=head1 ATTRIBUTES

=head2 cache_paths

  my $store     = $store->cache_paths(\@paths);
  my $array_ref = $store->cache_paths;

A list of directories to where cached specifications are stored. Defaults to
C<JSON_VALIDATOR_CACHE_PATH> environment variable and the specs that is bundled
with this distribution.

C<JSON_VALIDATOR_CACHE_PATH> can be a list of directories, each separated by ":".

See L<JSON::Validator/Bundled specifications> for more details.

=head2 schemas

  my $hash_ref = $store->schemas;
  my $store = $store->schemas({});

Hold the schemas as data structures. The keys are schema "id".

=head2 ua

  my $ua    = $store->ua;
  my $store = $store->ua(Mojo::UserAgent->new);

Holds a L<Mojo::UserAgent> object, used by L</schema> to load a JSON schema
from remote location.

The default L<Mojo::UserAgent> will detect proxy settings and have
L<Mojo::UserAgent/max_redirects> set to 3.

=head1 METHODS

=head2 add

  my $normalized_id = $store->add($id => \%schema);

Used to add a schema data structure. Note that C<$id> might not be the same as
C<$normalized_id>.

=head2 exists

  my $normalized_id = $store->exists($id);

Returns a C<$normalized_id> if it is present in the L</schemas>.

=head2 get

  my $schema = $store->get($normalized_id);

Used to retrieve a C<$schema> added by L</add> or L</load>.

=head2 load

  my $normalized_id = $store->load('https://...');
  my $normalized_id = $store->load('data://main/foo.json');
  my $normalized_id = $store->load('---\nid: yaml');
  my $normalized_id = $store->load('{"id":"yaml"}');
  my $normalized_id = $store->load(\$text);
  my $normalized_id = $store->load('/path/to/foo.json');
  my $normalized_id = $store->load('file:///path/to/foo.json');
  my $normalized_id = $store->load('/load/from/ua-server-app');

Can load a C<$schema> from many different sources. The input can be a string or
a string-like object, and the L</load> method will try to resolve it in the
order listed in above.

Loading schemas from C<$text> will generate an C<$normalized_id> in L</schemas>
looking like "urn:text:$text_checksum". This might change in the future!

Loading files from disk will result in a C<$normalized_id> that always start
with "file://".

Loading can also be done with relative path, which will then load from:

  $store->ua->server->app;

This method is EXPERIMENTAL, but unlikely to change significantly.

=head2 resolve

  $hash_ref = $store->resolve($url, \%defaults);

Takes a C<$url> (can also be a file, urn, ...) with or without a fragment and
returns this structure about the schema:

  {
    base_url => $str,  # the part before the fragment in the $url
    fragment => $str,  # fragment part of the $url
    id       => $str,  # store ID
    root     => ...,   # the root schema
    schema   => ...,   # the schema inside "root" if fragment is present
  }

This method is EXPERIMENTAL and can change without warning.

=head1 SEE ALSO

L<JSON::Validator>.

=cut