File Coverage

blib/lib/JSON/GRDDL.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1 2     2   49965 use 5.010;
  2         10  
  2         90  
2 2     2   12 use strict;
  2         3  
  2         68  
3 2     2   10 use warnings;
  2         8  
  2         98  
4              
5             package JSON::GRDDL;
6              
7 2     2   11 use Carp;
  2         4  
  2         227  
8 2     2   2343 use JSON;
  2         42916  
  2         19  
9 2     2   28925 use JSON::T;
  0            
  0            
10             use LWP::UserAgent;
11             use Object::AUTHORITY;
12             use RDF::Trine;
13             use Scalar::Util qw[blessed];
14              
15             BEGIN {
16             $JSON::GRDDL::AUTHORITY = 'cpan:TOBYINK';
17             $JSON::GRDDL::VERSION = '0.002';
18             }
19              
20             sub new
21             {
22             my ($class) = @_;
23             return bless { cache=>{}, ua=>undef, }, $class;
24             }
25              
26             sub ua
27             {
28             my $self = shift;
29             if (@_)
30             {
31             my $rv = $self->{'ua'};
32             $self->{'ua'} = shift;
33             croak "Set UA to something that is not an LWP::UserAgent!"
34             unless blessed $self->{'ua'} && $self->{'ua'}->isa('LWP::UserAgent');
35             return $rv;
36             }
37             unless (blessed $self->{'ua'} && $self->{'ua'}->isa('LWP::UserAgent'))
38             {
39             $self->{'ua'} = LWP::UserAgent->new(agent=>sprintf('%s/%s (%s) ',
40             __PACKAGE__,
41             __PACKAGE__->VERSION,
42             __PACKAGE__->AUTHORITY,
43             ));
44             }
45             return $self->{'ua'};
46             }
47              
48             sub data
49             {
50             my ($self, $document, $uri, %options) = @_;
51            
52             unless (ref $document)
53             {
54             $document = from_json("$document");
55             }
56            
57             $options{'model'} ||= RDF::Trine::Model->temporary_model;
58            
59             my $T = $self->discover($document, $uri, %options);
60             if ($T)
61             {
62             return $self->transform_by_uri($document, $uri, $T, %options);
63             }
64             elsif (ref $document eq 'HASH' and !$options{'nested'}
65             and (not grep { $_ !~ /:/ } keys %$document))
66             {
67             # looks like it's bona-fide RDF/JSON.
68             $options{'model'}->add_hashref($document);
69             return $options{'model'};
70             }
71             elsif (ref $document eq 'HASH'
72             and $document->{'$schema'}->{'$ref'} eq 'http://soapjr.org/schemas/RDF_JSON')
73             {
74             # claims it's bona-fide RDF/JSON.
75             $options{'model'}->add_hashref($document);
76             return $options{'model'};
77             }
78            
79             # Not returned anything yet, so try recursing.
80             {
81             local $options{'nested'} = 1;
82            
83             if (ref $document eq 'HASH')
84             {
85             foreach my $item (values %$document)
86             {
87             if ('HASH' eq ref $item or 'ARRAY' eq ref $item)
88             {
89             $self->data($item, $uri, %options);
90             }
91             }
92             }
93             elsif (ref $document eq 'ARRAY')
94             {
95             foreach my $item (@$document)
96             {
97             if ('HASH' eq ref $item or 'ARRAY' eq ref $item)
98             {
99             $self->data($item, $uri, %options);
100             }
101             }
102             }
103             }
104            
105             return $options{'model'};
106             }
107              
108             sub discover
109             {
110             my ($self, $document, $uri, %options) = @_;
111             my $T;
112            
113             unless (ref $document)
114             {
115             $document = from_json("$document");
116             }
117              
118             return unless ref $document eq 'HASH';
119            
120             if (defined $document->{'$transformation'})
121             {
122             $T = $self->_resolve_relative_ref($document->{'$transformation'}, $uri);
123             }
124             elsif (defined $document->{'$schema'}->{'$schemaTransformation'})
125             {
126             $T = $self->_resolve_relative_ref($document->{'$schema'}->{'$schemaTransformation'}, $uri);
127             }
128             elsif (defined $document->{'$schema'}->{'$ref'})
129             {
130             my $s = $self->_resolve_relative_ref($document->{'$schema'}->{'$ref'}, $uri);
131             my $r = $self->_fetch($s,
132             Accept => 'application/schema+json, application/x-schema+json, application/json');
133            
134             if (defined $r
135             && $r->code == 200
136             && $r->header('content-type') =~ m#^\s*(((application|text)/(x-)?json)|(application/(x-)?schema\+json))\b#)
137             {
138             my $schema = from_json($r->decoded_content);
139             if (defined $schema->{'$schemaTransformation'})
140             {
141             $T = $self->_resolve_relative_ref($schema->{'$schemaTransformation'}, $s);
142             }
143             }
144             }
145             return ($T);
146             }
147              
148             sub transform_by_uri
149             {
150             my ($self, $document, $uri, $transformation_uri, %options) = @_;
151            
152             my ($name) = ($transformation_uri =~ /\#(.+)$/);
153            
154             my $r = $self->_fetch($transformation_uri,
155             Accept => 'application/ecmascript, application/javascript, text/ecmascript, text/javascript, application/x-ecmascript');
156             if (defined $r
157             && $r->code == 200
158             && $r->header('content-type') =~ m#^\s*((application|text)/(x-)?(java|ecma)script)\b#)
159             {
160             return $self->transform_by_jsont($document, $uri, $r->decoded_content, $name, %options);
161             }
162            
163             return;
164             }
165              
166             sub transform_by_jsont
167             {
168             my ($self, $document, $uri, $transformation, $name, %options) = @_;
169            
170             my $jsont = JSON::T->new($transformation, $name);
171             my $out = $jsont->transform_structure($document);
172            
173             _relabel($out);
174            
175             $options{'model'} ||= RDF::Trine::Model->temporary_model;
176             $options{'model'}->add_hashref($out);
177             return $options{'model'};
178             }
179              
180             sub _relabel
181             {
182             my ($data) = @_;
183             my $pfx = '_:p'.int( 10_000_000 + rand(80_000_000) );
184            
185             foreach my $key (keys %$data)
186             {
187             if ($key =~ /^_:(.*)/)
188             {
189             my $new_key = $pfx . $1;
190             $data->{$new_key} = delete $data->{$key}
191             }
192             }
193            
194             foreach my $po (values %$data)
195             {
196             foreach my $ol (values %$po)
197             {
198             foreach my $o (@$ol)
199             {
200             next if $o->{type} =~ /literal/i;
201             next if exists $o->{lang};
202             next if exists $o->{datatype};
203            
204             if ($o->{value} =~ /^_:(.*)/)
205             {
206             $o->{value} = $pfx . $1;
207             }
208             }
209             }
210             }
211             }
212              
213             sub _fetch
214             {
215             my ($self, $document, %headers) = @_;
216             $self->{'cache'}->{$document} ||= $self->ua->get($document, %headers);
217             return $self->{'cache'}->{$document};
218             }
219              
220             sub _resolve_relative_ref
221             {
222             my ($self, $ref, $base) = @_;
223              
224             return $ref unless $base; # keep relative unless we have a base URI
225              
226             if ($ref =~ /^([a-z][a-z0-9\+\.\-]*)\:/i)
227             {
228             return $ref; # already an absolute reference
229             }
230              
231             # create absolute URI
232             my $abs = URI->new_abs($ref, $base)->canonical->as_string;
233              
234             while ($abs =~ m!^(http://.*)(\.\./|\.)+(\.\.|\.)?$!i)
235             { $abs = $1; } # fix edge case of 'http://example.com/../../../'
236              
237             return $abs;
238             }
239              
240             1;
241              
242             __END__
243              
244             =head1 NAME
245              
246             JSON::GRDDL - transform JSON to RDF
247              
248             =head1 SYNOPSIS
249              
250             # Low-Level Interface
251             #
252             my $grddl = JSON::GRDDL->new;
253             my @transformations = $grddl->discover($jsondoc, $baseuri);
254             foreach my $trans (@transformations)
255             {
256             my $model = $grddl->transform_by_uri($jsondoc, $baseuri, $trans);
257             # $model is an RDF::Trine::Model
258             }
259              
260             # High-Level Interface
261             #
262             my $grddl = JSON::GRDDL->new;
263             my $model = $grddl->data($jsondoc, $baseuri);
264             # $model is an RDF::Trine::Model
265              
266             =head1 DESCRIPTION
267              
268             This module implements jsonGRDDL, a port of GRDDL concepts from XML
269             to JSON.
270              
271             jsonGRDDL is described at L<http://buzzword.org.uk/2008/jsonGRDDL/spec>.
272              
273             This module attempts to provide a similar API to L<XML::GRDDL> but differs
274             in some respects.
275              
276             =head2 Constructor
277              
278             =over 4
279              
280             =item C<< JSON::GRDDL->new >>
281              
282             The constructor accepts no parameters and returns a JSON::GRDDL
283             object.
284              
285             =back
286              
287             =head2 Methods
288              
289             =over 4
290              
291             =item C<< $grddl->ua >>
292              
293             =item C<< $grddl->ua($ua) >>
294              
295             Get/set an L<LWP::UserAgent> object for HTTP requests.
296              
297             =item C<< $grddl->data($json, $base, %options) >>
298              
299             This is usually what you want to call. It's a high-level method that does everything
300             for you and returns the RDF you wanted. $json is the raw JSON source of the
301             document, or an equivalent Perl hashref/arrayref structure. $base is the base
302             URI for resolving relative references.
303              
304             Returns an RDF::Trine::Model.
305              
306             =item C<< $grddl->discover($json, $base, %options) >>
307              
308             You only need to call this method if you're doing something unusual.
309              
310             Processes the JSON document to discover the transformation associated
311             with it. $json is the raw JSON source of the document, or an equivalent
312             Perl hashref/arrayref structure. $base is the base URI for resolving relative
313             references.
314              
315             Returns a list of URLs as strings.
316              
317             =item C<< $grddl->transform_by_uri($json, $base, $transformation, %options) >>
318              
319             You only need to call this method if you're doing something unusual.
320              
321             Transforms a JSON document into RDF using a JsonT transformation, specified by
322             URI. $json is the raw JSON source of the document, or an equivalent
323             Perl hashref/arrayref structure. $base is the base URI for resolving relative
324             references. $transformation is the URI for the JsonT transformation.
325              
326             Returns an RDF::Trine::Model.
327              
328             =item C<< $grddl->transform_by_jsont($json, $base, $code, $name, %options) >>
329              
330             You only need to call this method if you're doing something unusual.
331              
332             Transforms a JSON document into RDF using a JsonT transformation, specified
333             as a Javascript code, variable name pair. $json is the raw JSON source of the
334             document, or an equivalent Perl hashref/arrayref structure. $base is the base
335             URI for resolving relative references. $code and $name must be suitable for
336             passing to the C<new> constructor from the L<JSON::T> package.
337              
338             Returns an RDF::Trine::Model.
339              
340             =back
341              
342             =head1 BUGS
343              
344             Please report any bugs to L<http://rt.cpan.org/>.
345              
346             =head1 SEE ALSO
347              
348             Specification: L<http://buzzword.org.uk/2008/jsonGRDDL/spec>.
349              
350             Related modules: L<JSON>, L<JSON::T>, L<JSON::Path>,
351             L<JSON::Hyper>, L<JSON::Schema>, L<XML::GRDDL>.
352              
353             L<http://www.perlrdf.org/>.
354              
355             This module is derived from Swignition L<http://buzzword.org.uk/swignition/>.
356              
357             =head1 AUTHOR
358              
359             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
360              
361             =head1 COPYRIGHT AND LICENCE
362              
363             Copyright 2008-2011 Toby Inkster.
364              
365             This library is free software; you can redistribute it and/or modify it
366             under the same terms as Perl itself.
367              
368             =head1 DISCLAIMER OF WARRANTIES
369              
370             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
371             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
372             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
373