File Coverage

blib/lib/JSON/Hyper.pm
Criterion Covered Total %
statement 62 144 43.0
branch 10 70 14.2
condition 2 19 10.5
subroutine 15 23 65.2
pod 8 8 100.0
total 97 264 36.7


line stmt bran cond sub pod time code
1             package JSON::Hyper;
2              
3 1     1   31979 use 5.008;
  1         4  
  1         48  
4 1     1   6 use strict;
  1         3  
  1         38  
5              
6 1     1   677 use JSON::Hyper::Link;
  1         2  
  1         31  
7              
8 1     1   8 use Carp;
  1         2  
  1         98  
9 1     1   1172 use JSON;
  1         20412  
  1         9  
10 1     1   1285 use JSON::Path;
  1         29107  
  1         14  
11 1     1   9414 use LWP::UserAgent;
  1         146630  
  1         40  
12 1     1   11 use Scalar::Util qw[blessed];
  1         3  
  1         72  
13 1     1   6 use Storable qw[dclone];
  1         3  
  1         72  
14 1     1   7 use URI;
  1         1  
  1         1205  
15 1     1   8 use URI::Escape qw[uri_unescape];
  1         2  
  1         3526  
16              
17             our $AUTHORITY = 'cpan:TOBYINK';
18             our $VERSION = '0.011';
19             our $DEBUG = 0;
20              
21             sub json_ref
22             {
23             return {
24 0     0 1 0 description => 'A hyper schema for the JSON referencing convention',
25             links => [
26             {
27             href => '{id}',
28             link => 'self',
29             },
30             {
31             href => '{$ref}',
32             link => 'full',
33             },
34             {
35             href => '{$schema}',
36             link => 'describedby',
37             },
38             ],
39             fragmentResolution => 'dot-delimited',
40             additionalProperties => { '$ref' => '#' },
41             };
42             }
43              
44             sub new
45             {
46 1     1 1 13 my ($class, $schema) = @_;
47 1   33     5 $schema ||= json_ref();
48 1 50       9 $schema = from_json($schema) unless ref $schema;
49 1         54 return bless { schema => $schema, ua => undef } => $class;
50             }
51              
52             sub schema
53             {
54 1     1 1 3 my ($self) = @_;
55 1         9 return $self->{'schema'};
56             }
57              
58             sub ua
59             {
60 0     0 1 0 my $self = shift;
61 0 0       0 $self = {} unless blessed($self);
62            
63 0 0       0 if (@_)
64             {
65 0         0 my $rv = $self->{'ua'};
66 0         0 $self->{'ua'} = shift;
67 0 0 0     0 croak "Set UA to something that is not an LWP::UserAgent!"
68             unless blessed $self->{'ua'} && $self->{'ua'}->isa('LWP::UserAgent');
69 0         0 return $rv;
70             }
71 0 0 0     0 unless (blessed $self->{'ua'} and $self->{'ua'}->isa('LWP::UserAgent'))
72             {
73 0         0 $self->{'ua'} = 'LWP::UserAgent'->new(
74             agent=>sprintf('%s/%s ', __PACKAGE__, $VERSION)
75             );
76 0         0 $self->{'ua'}->default_header(
77             'Accept'=>'application/json, application/schema+json',
78             );
79             }
80 0         0 return $self->{'ua'};
81             }
82              
83             sub find_links
84             {
85 1     1 1 25 my ($self, $node, $base) = @_;
86            
87 1 50       6 $node = from_json($node) unless ref $node;
88 1 50       7 return unless ref $node eq 'HASH';
89 1         2 my @rv;
90            
91 1         3 foreach my $link (@{ $self->schema->{links} })
  1         5  
92             {
93 5         10 my $missing = 0;
94 5         12 my $href = $link->{href};
95 5 100       118 $href =~ s/\{(.+?)\}/if (exists $node->{$1}) { $node->{$1}; } else { $missing++; ''; }/gex;
  6         24  
  5         19  
  1         2  
  1         4  
96            
97 5 100       16 if (!$missing)
98             {
99 4 50       18 $href = $self->_resolve_relative_ref($href, $base) if defined $base;
100            
101 4   33     128 push @rv, 'JSON::Hyper::Link'->new({
102             href => $href,
103             rel => ($link->{'rel'} || $link->{'link'}),
104             targetSchema => $link->{'targetSchema'},
105             method => $link->{'method'},
106             enctype => $link->{'enctype'},
107             schema => $link->{'schema'},
108             properties => $link->{'properties'},
109             });
110             }
111             }
112            
113 1         10 return @rv;
114             }
115              
116             sub _resolve_relative_ref
117             {
118 4     4   10 my ($self, $ref, $base) = @_;
119              
120 4 50       9 return $ref unless $base; # keep relative unless we have a base URI
121              
122 4 50       17 if ($ref =~ /^([a-z][a-z0-9\+\.\-]*)\:/i)
123             {
124 0         0 return $ref; # already an absolute reference
125             }
126              
127             # create absolute URI
128 4         23 my $abs = URI->new_abs($ref, $base)->canonical->as_string;
129              
130 4         15840 while ($abs =~ m!^(http://.*)(\.\./|\.)+(\.\.|\.)?$!i)
131 0         0 { $abs = $1; } # fix edge case of 'http://example.com/../../../'
132              
133 4         39 return $abs;
134             }
135              
136             sub process_includes
137             {
138 0     0 1   my ($self, $original, $base, $recurse) = @_;
139 0 0         $original = from_json($original) unless ref $original;
140 0           $self->_process_includes($original, $base, $recurse);
141 0           return $original;
142             }
143              
144             sub _process_includes
145             {
146 0     0     my ($self, $object, $base, $recurse) = @_;
147            
148 0           my @links = $self->find_links($object, $base);
149 0           my $full;
150 0           foreach my $link (@links)
151             {
152 0 0         if (lc $link->{rel} eq 'full')
153             {
154 0           $full = $link;
155 0           last;
156             }
157             }
158            
159 0 0         if (defined $full)
160             {
161 0           my ($substitute) = $self->get($full->{'href'});
162 0 0         if (defined $substitute)
163             {
164 0           delete $object->{ $full->{'property'} };
165 0           while (my($k,$v) = each %$substitute)
166             {
167 0           $object->{$k} = $v;
168             }
169             }
170 0           return;
171             }
172 0 0         return unless $recurse;
173              
174 0 0         if (ref $object eq 'ARRAY')
    0          
175             {
176 0           foreach my $i (@$object)
177             {
178 0           $self->_process_includes($i, $base, $recurse);
179             }
180             }
181             elsif (ref $object eq 'HASH')
182             {
183 0           foreach my $i (values %$object)
184             {
185 0           $self->_process_includes($i, $base, $recurse);
186             }
187             }
188             }
189              
190             sub get
191             {
192 0     0 1   my ($self, $uri) = @_;
193 0           my ($resource, $fragment) = split /\#/, $uri, 2;
194 0           my $object = $self->_get($resource);
195 0 0         return $object unless $fragment;
196 0           return $self->resolve_fragment($object, $fragment);
197             }
198              
199             sub _get
200             {
201 0     0     my ($self, $resource) = @_;
202            
203 0 0         warn "GETting $resource" if $DEBUG;
204            
205 0 0         unless ($self->{'cache'}->{$resource})
206             {
207 0           my $response = $self->ua->get($resource);
208 0 0         return unless $response->is_success;
209 0           $self->{'cache'}->{$resource} = from_json( $response->decoded_content );
210 0           $self->{'http_cache'}->{$resource} = $resource;
211             }
212            
213 0           my @r = ($self->{'cache'}->{$resource}, $self->{'http_cache'}->{$resource});
214 0 0         return wantarray ? @r : $r[0];
215             }
216              
217             sub resolve_fragment
218             {
219 0     0 1   my ($self, $object, $fragment) = @_;
220 0   0       my $style = $self->schema->{fragmentResolution} || 'slash-delimited';
221              
222 0 0         $object = from_json($object) unless ref $object;
223 0 0         return $object unless $fragment;
224              
225 0           $fragment =~ s!^#!!;
226              
227 0 0         if ($style =~ /^(json.?)?path$/i)
    0          
    0          
228             {
229 0           my $jsonp = JSON::Path->new(uri_unescape($fragment));
230 0           my @matches = $jsonp->values($object);
231 0           return @matches;
232             }
233             elsif (lc $style eq 'dot-delimited')
234             {
235 0           $fragment =~ s!^\.!!;
236             }
237             elsif (lc $style eq 'slash-delimited')
238             {
239 0           $fragment =~ s!^/!!;
240             }
241             else
242             {
243 0           carp "Unknown fragment resolution method: $style";
244 0           return;
245             }
246            
247 0           return $self->_resolve_fragment($object, $fragment);
248             }
249              
250             sub _resolve_fragment
251             {
252 0     0     my ($self, $object, $fragment) = @_;
253 0   0       my $style = $self->schema->{fragmentResolution} || 'slash-delimited';
254            
255 0           my ($first, $rest);
256 0 0         if (lc $style eq 'dot-delimited')
    0          
257             {
258 0           ($first, $rest) = split /\./, $fragment, 2;
259             }
260             elsif (lc $style eq 'slash-delimited')
261             {
262 0           ($first, $rest) = split /\//, $fragment, 2;
263             }
264              
265 0           $first = uri_unescape($first);
266              
267 0           my $value;
268 0 0 0       if (ref $object eq 'HASH')
    0          
269             {
270 0           $value = $object->{$first};
271             }
272             elsif (ref $object eq 'ARRAY' and $first =~ /^[\-\+]?[0-9]+$/)
273             {
274 0           $value = $object->[$first];
275             }
276            
277 0 0         unless (defined $value)
278             {
279 0           return;
280             }
281            
282 0 0         if (length $rest)
283             {
284 0           return $self->_resolve_fragment($value, $rest);
285             }
286             else
287             {
288 0           return ($value);
289             }
290             }
291              
292             1;
293              
294             __END__