File Coverage

blib/lib/JRPC/Client.pm
Criterion Covered Total %
statement 27 96 28.1
branch 0 34 0.0
condition 0 15 0.0
subroutine 9 17 52.9
pod 2 5 40.0
total 38 167 22.7


line stmt bran cond sub pod time code
1             # Send Requests to a JSON-RPC Service.
2             # We completely ride on the wonderful LWP Module.
3             {
4             package JRPC::Client;
5             #
6 1     1   733 use LWP;
  1         1  
  1         23  
7 1     1   4 use LWP::UserAgent;
  1         1  
  1         20  
8 1     1   4 use base ('LWP::UserAgent');
  1         1  
  1         80  
9 1     1   5 use JSON::XS;
  1         1  
  1         37  
10 1     1   4 use Data::Dumper;
  1         2  
  1         298  
11              
12             #our $mime;
13             #BEGIN {
14             # De-facto JSON-RPC Mime type
15             our $mime = 'application/json';
16             #};
17              
18             =head1 NAME
19              
20             JRPC::Client - JSON-RPC 2.0 Client
21              
22             =head1 SYNOPSIS
23              
24             use JRPC::Client;
25              
26             my $client = JRPC::Client->new();
27             $req = $client->new_request("http://jservices.com/WorldTime");
28             my $resp = $req->call('Timeinfo.getlocaltime', {'tzname' => 'CET', 'clockhrs' => '24'});
29             if (my $err = $resp->error()) { die("$err->{'message'}"); }
30             my $res = $resp->result();
31             print("Local time in CET is: $res->{'timeiso'}\n");
32              
33             =head1 DESCRIPTION
34              
35             JRPC::Client is a Perl LWP based JSON-RPC 2.0 Client hoping to minimize tedious boilerplate code for JSON-RPC
36             interaction, yet enabling advanced use cases by the power of LWP / HTTP::Request.
37              
38             JRPC::Client complies to conventions of JSON-RPC 2.0, but it can be coerced to be used for other versions as well.
39              
40             =head2 $client = JRPC::Client->new()
41              
42             Instantiate a new JSON-RPC (2.0) Client.
43             HTTP keep-alive is turned on, cookie store is established and
44             default user-agent name is set here.
45             Any of the LWP::UserAgent methods are callable on the returned object as JRPC::Client IS-A LWP::UserAgent.
46              
47             The lifetime of the JRPC::Client can be kept long (e.g. throughout app) and it can usually be kept as single instance
48             in app runtime (singleton, however JRPC::Client does not control singularity of instantiation).
49             The factory method method new_request() takes care of instatiating requests for various URL:s, various methods.
50              
51             =cut
52             sub new {
53 0     0 1   my ($class, %c) = @_;
54 0           my $ua = LWP::UserAgent->new('keep_alive' => 1, 'cookie_jar' => {});
55 0           $ua->agent("JSON-RPC Client/0.9");
56 0 0         if ($c{'jsonrpc'}) {$ua->{'_jsonrpc'} = $c{'jsonrpc'};}
  0            
57             # Re-bless ...
58 0           return bless($ua, $class);
59             }
60              
61             =head2 $req = $client->new_request($url, %opts)
62              
63             Factory method to instantiate and prepare a new JSON-RPC request to a URL. Options in %opts:
64              
65             =over 4
66              
67             =item * 'mime' - Mime content-type for request (default: 'application/json')
68              
69             =item * 'debug' - Dump Request after instantiation (to STDERR).
70              
71             =back
72              
73              
74              
75             =cut
76             sub new_request {
77 0     0 1   my ($ua, $url, %c) = @_;
78             # 'mime' - Special mime type to use (default: 'application/json')
79 0           my $req = HTTP::Request->new('POST', $url);
80            
81 0   0       $req->content_type($c{'mime'} || $mime); # text/plain
82             #if ($c{'cred'}) {$req->header('Authorization', "Basic $c{'cred'}");}
83             # Need to associate agent to request for call-phase
84 0           $req->{'_ua'} = $ua;
85             # Rebless to JRPC::Client::Request. @ISA / use base takes care of HTTP::Request methods being callable.
86 0           bless($req, 'JRPC::Client::Request');
87 0 0         if ($c{'debug'}) {print(STDERR Dumper($req));} # Store persistently: $req->{'_jsonrpcdebug'} = $c{'debug'};
  0            
88             # Verification / Sanity check
89 0 0         if (!$req->isa('HTTP::Request')) {die("NOT a HTTP::Request");}
  0            
90 0           return($req);
91             }
92              
93             };
94             #############
95             {
96             package JRPC::Client::Request;
97 1     1   5 use Data::Dumper;
  1         1  
  1         35  
98 1     1   3 use JSON::XS;
  1         1  
  1         32  
99 1     1   4 use strict;
  1         0  
  1         22  
100 1     1   3 use warnings;
  1         1  
  1         512  
101             our @ISA = ('HTTP::Request');
102             our $id = 1;
103             our $debug = 0;
104              
105             # NOTREALLY: Override the famous is_success() / is_error() methods.
106             # Because the JSON-RPC is higher level than HTTP, we are not talking about
107             # about HTTP success (200 success vs. 500 Error), but JSON-RPC success/error.
108             # NEW: This is probably bad idea as is_success / is_error are very established
109             # and besides useful for detecting HTTP level errors.
110             #sub is_success {
111             # my ($req) = @_;
112             #
113             #}
114              
115             =head2 $resp = $req->call($method, $params, %opts)
116              
117             Call a method previously prepared as a HTTP::Request on a URL (see new_request()).
118             The JSON-RPC parameters passed as $param may be either a perl data structure (reference) or a filename (string).
119              
120             =over 4
121              
122             =item * Valid JSON string
123              
124             =item * a Perl runtime data-structure with JSON serializable elements.
125              
126             =back
127              
128             In either case above (as a bit of forgiving behaviour) also passing a complete
129             JSON-RPC message is allowed for covenience. A complete JSON-RPC message is
130             detected by the presence of members 'id', 'jsonrpc', 'params' and 'method', which
131             (especially all at the same time, together) are extremely unlikely to appear
132             in the parameters. In the case of passing a complete message, the method found in
133             message overrides the $meth passed params.
134              
135             Optional KW parameters in %opts:
136              
137             =over 4
138              
139             =item * notify - Treat call as JSON-RPC notification. Ignore response (do not parse it).
140              
141             =item * debug - Produce debug output for call() phase
142              
143             =back
144              
145             Note: on regular call (i.e. non-notification by 'notify' => 0) call() method parses the JSON
146             response and expects it to be valid JSON, but it does not validate the JSON-RPC envelope
147             (for presence of mandatory members).
148              
149             Return (LWP) HTTP response object.
150              
151             Further access by $resp->result() will evaluate the validity of the envelope.
152              
153             =cut
154             # the "params" section of JSON-RPC message or
155             # for convenience a complete JSON-RPC message (i.e. envelope with members "jsonrpc","method","id","params").
156             # TODO: Support non-forgiving behaviour.
157             sub call {
158 0     0     my ($req, $meth, $param, %c) = @_;
159 0           my ($msg, $pp, $len);
160 0           my $isref = ref($param);
161             #if ($isref) {}
162             # Risk it and accept string form json as likely prevalidated JSON.
163             # Die on parsing errors by JSON::XS.
164 0 0 0       if (!$isref && $param =~ /^\s*{/) {
  0 0          
165 0           $pp = eval { decode_json($param); };
  0            
166 0 0         if ($@) {die("Error In JSON params passed as string");}
  0            
167             }
168 0           elsif ($isref) {$pp = $param;}
169             else {die("Malformed JSON body ($param)");}
170 0           my %enpara = ();
171 0 0         if ($c{'id'}) {$enpara{'id'} = $c{'id'};} # Allow explicit id
  0            
172             # Forgiving mode - accept complete message
173 0 0         if (is_message($pp)) {$msg = $pp;}
  0            
  0            
174             else {$msg = envelope($meth, $pp, %enpara);}
175             # eval for catching serialization errors (for example blessed
176             # branches w/o TO_JSON for type).
177 0           my $body = eval { encode_json($msg); };
  0            
178 0 0         if ($@) {die("Error Serializing message: $@");}
  0            
179 0           $len = length($body);
180             #my $len = length($body);
181 0           $req->content($body);
182 0           $req->header('content-length', $len);
183 0           my $ua = $req->{'_ua'};
184 0 0         if (!$ua) {die("Missing User-Agent for call() phase");}
  0            
185             ############# Launch Request !
186 0           my $res = $ua->request($req);
187 0 0         if ($c{'debug'}) {print(STDERR Dumper($res));}
  0            
188             # Call directly ... Request:..
189 0 0         if ($res->is_success()) {
  0            
190             # Parse Response in case of success
191             # (OR ALWYAYS on any HTTP status ?)
192 0 0 0       if ($debug || $c{'debug'}) {print(STDERR "Response-Content:\n=====\n".$res->content()."\n=====\n");}
  0            
193             # Allow request to be a notification - Ignore response and do NOT parse it.
194             # In this case Client should not call $resp->result()
195 0 0         if ($c{'notify'}) {return($res);} # Or goto
  0            
196 0           my $respmsg = $res->{'_parsed_content'} = eval { decode_json($res->content()); };
  0            
197 0 0         if ($@) {die("Error parsing reponse: $@");}
  0            
198             #$res->{'_parsed_content'}
199             # Even in case of is_success() true, check for 'error' (exception)
200             #if (my $error = $respmsg->{'error'}) {
201             # $res->{'_parsed_response'} = $error;
202             #}
203             #else {
204             # $res->{'_parsed_response'} = $respmsg->{'result'};
205             #}
206             }
207             # HTTP Errors (as interpreted by LWP)
208             else {die("JSON-RPC Error: ".$res->status_line());}
209 0           return($res);
210             }
211              
212             =head1 RESPONSE METHODS
213              
214             These methods magically appear in the HTTP::Response for the purposes of
215             JRPC::Client::Request.
216              
217             =cut
218              
219             #=head2 $resp->parsed_content();
220             sub HTTP::Response::parsed_content {
221 0     0 0   return($_[0]->{'_parsed_content'});
222             }
223              
224             =head2 $resp->result()
225              
226             JSON_RPC response "result" (as native data structure)
227              
228             =cut
229             sub HTTP::Response::result {
230 0     0 0   return($_[0]->{'_parsed_content'}->{'result'});
231             }
232              
233             =head2 $resp->error()
234              
235             JSON_RPC response "error" (as native data structure)
236              
237             =cut
238             sub HTTP::Response::error {
239 0     0 0   return($_[0]->{'_parsed_content'}->{'error'});
240             }
241              
242              
243             =head1 INTERNAL METHODS
244              
245             These methods should not be of interest to a user of the productivity API
246             (as demonstrated in SYNOPSIS).
247              
248             =head2 is_message($msg)
249              
250             Internal check to see if the passed structure looks like a JSON-RPC message envelope.
251             To do so, the handle must be a ref to a hash and contain envelope parameters
252             'id', 'jsonrpc', 'params' and 'method'.
253             is_message() is used to differentiate between complete
254             messages and parameters-only to provide a forgiving behaviour on higher level
255             functions (see call() method)
256              
257             =cut
258              
259             sub is_message {
260 0     0     my ($m) = @_;
261             # MUST Also be a ref eq 'HASH'
262 0 0         if (ref($m) ne 'HASH') {return(0);}
  0            
263 0   0       return($m->{'id'} && $m->{'jsonrpc'} && $m->{'params'} && $m->{'method'});
264             }
265              
266             =head2 envelope($meth, $params, %opts)
267              
268             Internal method to generate message envelope for method $meth and parameters passed.
269             The $params should be checked by is_message() first to have the correct
270             (non double wrapped) envelope created here.
271              
272             Method in $meth must be passed to generate message envelope.
273              
274             =cut
275             sub envelope {
276 0     0     my ($meth, $params, %c) = @_;
277 0 0         if (!$meth) {die("No 'method' member for envelope");}
  0            
278 0           my $msg = {'jsonrpc' => '2.0', 'method' => $meth, 'params' => $params, };
279             # Add ID - Either sequential / auto incrementing or explicitly passed.
280 0   0       $msg->{'id'} = $c{'id'} || ++$id;
281 0           return($msg);
282             }
283              
284              
285              
286             }; # end of JRPC::Client::Request
287             1;