File Coverage

blib/lib/RPC/JSON.pm
Criterion Covered Total %
statement 77 148 52.0
branch 22 60 36.6
condition 6 25 24.0
subroutine 13 19 68.4
pod 9 9 100.0
total 127 261 48.6


line stmt bran cond sub pod time code
1             package RPC::JSON;
2              
3 2     2   1653 use warnings;
  2         4  
  2         76  
4 2     2   11 use strict;
  2         2  
  2         57  
5              
6 2     2   1009 use RPC::JSON::Shell;
  2         7  
  2         65  
7              
8 2     2   18 use Carp;
  2         5  
  2         127  
9 2     2   27 use JSON;
  2         5  
  2         18  
10 2     2   3121 use LWP::UserAgent;
  2         110479  
  2         73  
11              
12 2     2   23 use URI;
  2         4  
  2         56  
13 2     2   1583 use URI::Heuristic qw(uf_uri);
  2         8658  
  2         229  
14              
15 2     2   148 use vars qw|$VERSION @EXPORT $DEBUG $META $AUTOLOAD|;
  2         81  
  2         5275  
16              
17             $VERSION = '0.15';
18              
19             @RPC::JSON = qw|Exporter|;
20              
21             @EXPORT = qw|
22             shell
23             test
24             |;
25              
26             our $REQUEST_COUNT = 1;
27              
28             =head1 NAME
29              
30             RPC::JSON - JSON-RPC Client Library
31              
32             =head1 SYNOPSIS
33              
34             use RPC::JSON;
35              
36             my $jsonrpc = RPC::JSON->new(
37             "http://www.simplymapped.com/services/geocode/json.smd" );
38              
39             # Imports a geocode(['address']) method:
40             $jsonrpc->geocode('1600 Pennsylvania Ave');
41              
42             Dumping this function returns whatever data was returned from the server.
43             In this case:
44              
45             $VAR1 = [
46             {
47             'administrativearea' => 'DC',
48             'country' => 'US',
49             'longitude' => '-77.037691',
50             'subadministrativearea' => 'District of Columbia',
51             'locality' => 'Washington',
52             'latitude' => '38.898758',
53             'thoroughfare' => '1600 Pennsylvania Ave NW',
54             'postalcode' => '20004',
55             'address' => '1600 Pennsylvania Ave NW, Washington, DC 20004, USA'
56             }
57             ];
58              
59             =head1 DESCRIPTION
60              
61             RPC::JSON aims to be a full-featured JSON-RPC client library that enables a
62             client to connect to any JSON-RPC service and dispatch remote method calls.
63              
64             =head1 METHODS
65              
66             =over
67              
68             =cut
69              
70             =item shell
71              
72             Instantiate a JSON-RPC shell
73              
74             =cut
75              
76             sub shell {
77 0     0 1 0 my ( $self ) = @_;
78 0         0 RPC::JSON::Shell::shell();
79             }
80              
81             my @options = qw|
82             smd timeout keepalive env_proxy agent conn_cache max_size dont_connect
83             |;
84              
85             =item new()
86              
87             Return a new RPC::JSON object for a given SMD source
88              
89             =cut
90              
91             sub new {
92 3     3 1 2519 my ( $class, @opts ) = @_;
93 3         16 my $self = {
94             utf8 => 0,
95             };
96              
97 3 50       14 unless ( @opts ) {
98 0         0 carp __PACKAGE__ . " requires at least the SMD URI";
99 0         0 return 0;
100             }
101              
102             # ->new({ smd => $SMDURI, timeout => $TIMEOUT });
103 3 100 66     43 if ( ref $opts[0] eq 'HASH' and @opts == 1 ) {
    100          
    50          
104 1         3 foreach my $key ( @options ) {
105 8 100       30 if ( exists $opts[0]->{$key} ) {
106 1         5 $self->{$key} = $opts[0]->{$key};
107             }
108             }
109             }
110             # ->new( smd => $SMDURI, timeout => $TIMEOUT );
111             elsif ( @opts % 2 == 0 ) {
112 1         4 my %p = @opts;
113 1         3 my $i = 0;
114 1         4 foreach my $key ( @options ) {
115 1 50       6 if ( $opts[$i] eq $key ) {
116 1         6 $self->{$key} = $opts[$i + 1];
117 1         3 $i += 2;
118             }
119 1 50       5 last unless $opts[$i];
120             }
121 1 50       7 unless ( keys %$self ) {
122 0         0 $self->{smd} = $opts[0];
123 0         0 $self->{timeout} = $opts[1];
124             }
125             }
126             # Called like:
127             # ->new( $SMDURI, $TIMEOUT );
128             elsif ( @opts < 2 ) {
129 1         21 $self->{smd} = $opts[0];
130 1         3 $self->{timeout} = $opts[1];
131             }
132 3         8 bless $self, $class;
133              
134             # Verify the SMD is valid
135 3 50       15 if ( $self->{smd} ) {
136 3         6 my $smd = $self->{smd};
137 3         8 delete $self->{smd};
138 3         12 $self->set_smd($smd);
139             }
140              
141 3 50       17 unless ( $self->{smd} ) {
142 0         0 carp "No valid SMD source, please check the SMD URI.";
143 0         0 return 0;
144             }
145             # Default timeout of 180 seconds
146 3   50     41 $self->{timeout} ||= 180;
147              
148 3 50       13 unless ( $self->{dont_connect} ) {
149             # If we fail to connect, it will alert the user but we shouldn't cancel
150             # the object (or maybe we should if it is a 40* error?)
151 3         12 $self->connect;
152             }
153 3         16 return $self;
154             }
155              
156             =item set_smd
157              
158             Sets the current SMD file, via URI
159              
160             =cut
161              
162             sub set_smd {
163 3     3 1 6 my ( $self, $smd ) = @_;
164 3         6 my $uri;
165 3         6 eval {
166 3 50       18 if ( $smd =~ /^\w+:/ ) {
167 3         19 $uri = new URI($smd);
168             } else {
169 0         0 $uri = uf_uri($smd);
170             }
171             };
172 3 50 33     13098 if ( $@ or not $uri ) {
173 0         0 carp $@;
174 0         0 return 0;
175             }
176 3         38 $self->{smd} = $uri;
177             }
178              
179             =item connect ?SMD?
180              
181             Connects to the specified SMD file, or whichever was configured with. This
182             will initialize the JSON-RPC service.
183              
184             =cut
185              
186             sub connect {
187 3     3 1 7 my ( $self, $smd ) = @_;
188 3 50       9 if ( $smd ) {
189 0         0 $self->set_smd($smd);
190             }
191 3         19 my %options =
192 24 100       148 map { $_ => $self->{$_} }
193 3         10 grep { $_ !~ '^smd|dont_connect$' and exists $self->{$_} }
194             @options;
195 3         36 $self->{_ua} = LWP::UserAgent->new( %options );
196 3 50 33     5163 if ( $self->{_ua} and $self->{smd} ) {
197 3         49 my $response = $self->{_ua}->get( $self->{smd} );
198            
199 3 50 33     120424 if ( $response and $response->is_success ) {
200 0         0 return $self->load_smd($response);
201             }
202              
203 3         62 carp "Can't load $self->{smd}: " . $response->status_line;
204             }
205 3         1268 return 0;
206             }
207              
208             =item load_smd
209              
210             load_smd will process a given SMD file by converting from JSON to a Perl
211             native structure, and setup the various keys as well as the autoload handles
212             for calling the methods.
213              
214             =cut
215              
216             sub load_smd {
217 0     0 1 0 my ( $self, $res ) = @_;
218 0         0 my $content = $res->content;
219             # Turn this on, because a lot of sources don't properly quote keys
220 0         0 local $JSON::BareKey = 1;
221 0         0 local $JSON::QuotApos = 1;
222 0         0 my $obj;
223 0         0 eval { $obj = from_json($content,{ utf8 => $self->is_utf8 }) };
  0         0  
224 0 0       0 if ( $@ ) {
225 0         0 carp $@;
226 0         0 return 0;
227             }
228 0 0       0 if ( $obj ) {
229 0         0 $self->{_service} = { methods => [] };
230 0         0 foreach my $req ( qw|serviceURL serviceType objectName SMDVersion| ) {
231 0 0       0 if ( $obj->{$req} ) {
232 0         0 $self->{_service}->{$req} = $obj->{$req};
233             } else {
234 0         0 carp "Invalid SMD format, missing key: $req";
235 0         0 return 0;
236             }
237             }
238 0 0       0 unless ( $self->{_service}->{serviceURL} =~ /^\w+:/ ) {
239 0         0 my $serviceURL = sprintf("%s://%s%s",
240             $self->{smd}->scheme,
241             $self->{smd}->authority,
242             $self->{_service}->{serviceURL});
243 0         0 $self->{_service}->{serviceURL} = $serviceURL;
244             }
245 0         0 $self->{serviceURL} = new URI($self->{_service}->{serviceURL});
246              
247 0         0 $self->{methods} = {};
248 0         0 foreach my $method ( @{$obj->{methods}} ) {
  0         0  
249 0 0 0     0 if ( $method->{name} and $method->{parameters} ) {
250 0         0 push @{$self->{_service}->{methods}}, $method;
  0         0  
251 0         0 $self->{methods}->{$method->{name}} = $self->{_service}->{methods}->[-1];
252             }
253             };
254             }
255 0         0 return 1;
256             }
257              
258             =item is_utf8
259              
260             makes the call to from_json utf8 aware (see perldoc JSON)
261              
262             $jsonrpc->is_utf8( 1 );
263              
264             default state is non utf8
265              
266             =cut
267              
268             sub is_utf8 {
269              
270 0     0 1 0 my ( $self,$set_utf8 ) = @_;
271 0 0       0 $self->{_utf8} = 1 if ( $set_utf8 );
272 0   0     0 return $self->{_utf8} || 0;
273             }
274              
275             =item service
276              
277             Return the object name of the current service connected to, or undef if
278             not connected.
279              
280             =cut
281              
282             sub service {
283 0     0 1 0 my ( $self ) = @_;
284 0 0 0     0 if ( $self->{_service} and $self->{_service}->{objectName} ) {
285 0         0 return $self->{_service}->{objectName};
286             }
287 0         0 return undef;
288             }
289              
290             =item methods
291              
292             Return a structure of method names for use on the current service, or undef
293             if not connected.
294              
295             The structure looks like:
296             {
297             methodName1 => [ { name => NAME, type => DATATYPE }, ... ]
298             }
299              
300             =cut
301              
302             sub methods {
303 0     0 1 0 my ( $self ) = @_;
304            
305 0 0 0     0 if ( $self->{_service} and $self->{_service}->{methods} ) {
306             return {
307 0         0 map { $_->{name} => $_->{parameters} }
  0         0  
308 0         0 @{$self->{_service}->{methods}}
309             };
310             }
311 0         0 return undef;
312             }
313              
314             =item serviceURI
315              
316             Returns the serviceURI (not the SMD URI, the URI to request RPC calls against),
317             or undef if not connected.
318              
319             =cut
320              
321             sub serviceURI {
322 0     0 1 0 my ( $self ) = @_;
323 0 0       0 if ( $self->{serviceURL} ) {
324 0         0 return $self->{serviceURL};
325             }
326 0         0 return undef;
327             }
328              
329             # TODO: Remove this and create generated methods. Although when we refresh
330             # the methods will need to be removed.
331             sub AUTOLOAD {
332 1     1   641 my $self = shift;
333 1         4 my ( $l ) = $AUTOLOAD;
334 1         9 $l =~ s/.*:://;
335 1 50       9 if ( exists $self->{methods}->{$l} ) {
336 0         0 my ( @p ) = @_;
337 0         0 my $packet = {
338             id => $REQUEST_COUNT++,
339             method => $l,
340             params => [ @p ]
341             };
342 0         0 my $res = $self->{_ua}->post(
343             $self->{serviceURL}->as_string,
344             Content_Type => 'application/javascript+json',
345             Content => to_json($packet)
346             );
347 0 0       0 if ( $res->is_success ) {
348 0         0 my $ret = {};
349 0         0 eval { $ret = from_json($res->content, { utf8 => $self->is_utf8 }) };
  0         0  
350 0 0       0 if ( $@ ) {
351 0         0 carp "Error parsing server response, but got acceptable status: $@";
352             } else {
353 0         0 my $result = from_json($ret->{result}, { utf8 => $self->is_utf8 });
354 0 0       0 return $result if $result;
355             }
356             } else {
357 0         0 carp "Error received from server: " . $res->status_line;
358             }
359             }
360 1         163 return undef;
361             }
362              
363             =back
364              
365             =head1 AUTHORS
366              
367             J. Shirley C<< >>
368              
369             =head1 CONTRIBUTORS
370              
371             Chris Carline
372             Lee Johnson
373              
374             =head1 LICENSE
375              
376             Copyright 2006-2008 J. Shirley C<< >>
377              
378             This program is free software; you can redistribute it and/or modify it under
379             the same terms as Perl itself. That means either (a) the GNU General Public
380             License or (b) the Artistic License.
381              
382             =cut
383              
384             1;