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
|
|
649
|
use LWP; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
26
|
|
7
|
1
|
|
|
1
|
|
4
|
use LWP::UserAgent; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
23
|
|
8
|
1
|
|
|
1
|
|
4
|
use base ('LWP::UserAgent'); |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
81
|
|
9
|
1
|
|
|
1
|
|
5
|
use JSON::XS; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
38
|
|
10
|
1
|
|
|
1
|
|
3
|
use Data::Dumper; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
207
|
|
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
|
|
|
|
|
34
|
|
98
|
1
|
|
|
1
|
|
16
|
use JSON::XS; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
32
|
|
99
|
1
|
|
|
1
|
|
4
|
use strict; |
|
1
|
|
|
|
|
0
|
|
|
1
|
|
|
|
|
23
|
|
100
|
1
|
|
|
1
|
|
3
|
use warnings; |
|
1
|
|
|
|
|
1
|
|
|
1
|
|
|
|
|
555
|
|
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; |