File: Store.pm

package info (click to toggle)
libjson-validator-perl 4.14%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 828 kB
  • sloc: perl: 2,816; makefile: 14
file content (244 lines) | stat: -rw-r--r-- 6,608 bytes parent folder | download
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
package JSON::Validator::Store;
use Mojo::Base -base;

use Mojo::Exception;
use Mojo::File qw(path);
use Mojo::JSON;
use Mojo::UserAgent;
use Mojo::Util qw(url_unescape);
use JSON::Validator::Schema;
use JSON::Validator::Util qw(data_section);

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("Unable to load schema $_[1]");
}

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

  my ($self, $url, $id) = @_;
  return undef unless $self->ua->server->app;
  return $id if $id = $self->exists($url);

  my $tx  = $self->ua->get($url);
  my $err = $tx->error && $tx->error->{message};
  _raise($err) if $err;
  return $self->add($url => _parse($tx->res->body));
}

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

  my ($self, $url, $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;
  return $self->add($url => _parse($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 = Mojo::URL->new->scheme('file')->host('')->path(CASE_TOLERANT ? lc $file : "$file");
  return $self->exists($id) || $self->add($id => _parse($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 = sprintf 'urn:text:%s', Mojo::Util::md5_sum($is_scalar_ref ? $$text : $text);
  return $self->exists($id) || $self->add($id => _parse($is_scalar_ref ? $$text : $text));
}

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

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

  $url = Mojo::URL->new($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;
    return $self->add($url => _parse($path->slurp)) if -r $path;
  }

  my $tx  = $self->ua->get($url);
  my $err = $tx->error && $tx->error->{message};
  _raise($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);
  }

  return $self->add($url => _parse($tx->res->body));
}

sub _parse {
  return Mojo::JSON::decode_json($_[0]) if $_[0] =~ m!^\s*\{!s;
  return JSON::Validator::Util::_yaml_load($_[0]);
}

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.

=head1 SEE ALSO

L<JSON::Validator>.

=cut