File Coverage

blib/lib/JSON/RPC2/Client.pm
Criterion Covered Total %
statement 119 119 100.0
branch 72 72 100.0
condition 34 34 100.0
subroutine 17 17 100.0
pod 10 10 100.0
total 252 252 100.0


line stmt bran cond sub pod time code
1             package JSON::RPC2::Client;
2              
3 15     15   790307 use warnings;
  15         41  
  15         631  
4 15     15   89 use strict;
  15         32  
  15         491  
5 15     15   87 use Carp;
  15         34  
  15         1525  
6              
7 15     15   14109 use version; our $VERSION = qv('1.0.0'); # update Changes & README
  15         28591  
  15         106  
8              
9             # update DEPENDENCIES in POD & Makefile.PL & README
10 15     15   5321 use JSON::XS;
  15         12816  
  15         1109  
11 15     15   95 use Scalar::Util qw( weaken refaddr );
  15         34  
  15         26181  
12              
13              
14             sub new {
15 12     12 1 154 my ($class) = @_;
16 12         89 my $self = {
17             next_id => 0,
18             free_id => [],
19             call => {},
20             id => {},
21             };
22 12         59 return bless $self, $class;
23             }
24              
25             sub batch {
26 11     11 1 2663 my ($self, @requests) = @_;
27 11         23 my @call = grep {ref} @requests;
  45         86  
28 11         22 @requests = grep {!ref} @requests;
  45         95  
29 11 100       65 croak 'at least one request required' if !@requests;
30 9         36 my $request = '['.join(q{,}, @requests).']';
31 9         49 return ($request, @call);
32             }
33              
34             sub notify {
35 11     11 1 33706 my ($self, $method, @params) = @_;
36 11 100       78 croak 'method required' if !defined $method;
37 10 100       168 return encode_json({
38             jsonrpc => '2.0',
39             method => $method,
40             (!@params ? () : (
41             params => \@params,
42             )),
43             });
44             }
45              
46             sub notify_named {
47 13     13 1 23699 my ($self, $method, @params) = @_;
48 13 100       68 croak 'method required' if !defined $method;
49 12 100       62 croak 'odd number of elements in %params' if @params % 2;
50 10         32 my %params = @params;
51 10 100       167 return encode_json({
52             jsonrpc => '2.0',
53             method => $method,
54             (!@params ? () : (
55             params => \%params,
56             )),
57             });
58             }
59              
60             sub call {
61 44     44 1 34672 my ($self, $method, @params) = @_;
62 44 100       203 croak 'method required' if !defined $method;
63 42         133 my ($id, $call) = $self->_get_id();
64 42 100       5339 my $request = encode_json({
65             jsonrpc => '2.0',
66             method => $method,
67             (!@params ? () : (
68             params => \@params,
69             )),
70             id => $id,
71             });
72 42 100       294 return wantarray ? ($request, $call) : $request;
73             }
74              
75             sub call_named {
76 28     28 1 32204 my ($self, $method, @params) = @_;
77 28 100       127 croak 'method required' if !defined $method;
78 26 100       115 croak 'odd number of elements in %params' if @params % 2;
79 24         74 my %params = @params;
80 24         76 my ($id, $call) = $self->_get_id();
81 24 100       294 my $request = encode_json({
82             jsonrpc => '2.0',
83             method => $method,
84             (!@params ? () : (
85             params => \%params,
86             )),
87             id => $id,
88             });
89 24 100       193 return wantarray ? ($request, $call) : $request;
90             }
91              
92             sub _get_id {
93 66     66   121 my $self = shift;
94 66 100       94 my $id = @{$self->{free_id}} ? pop @{$self->{free_id}} : $self->{next_id}++;
  66         328  
  12         31  
95 66         135 my $call = {};
96 66         450 $self->{call}{ refaddr($call) } = $call;
97 66         184 $self->{id}{ $id } = $call;
98 66         225 weaken($self->{id}{ $id });
99 66         192 return ($id, $call);
100             }
101              
102             sub pending {
103 6     6 1 30 my ($self) = @_;
104 6         7 return values %{ $self->{call} };
  6         61  
105             }
106              
107             sub cancel {
108 13     13 1 9407 my ($self, $call) = @_;
109 13 100       141 croak 'no such request' if !delete $self->{call}{ refaddr($call) };
110 9         33 return;
111             }
112              
113             sub batch_response {
114 18     18 1 10275 my ($self, $json) = @_;
115 18 100       80 croak 'require 1 param' if @_ != 2;
116              
117 16         23 undef $@;
118 16 100       40 my $response = ref $json ? $json : eval { decode_json($json) };
  15         116  
119 16 100       38 if ($@) {
120 5         25 return [ 'Parse error' ];
121             }
122 11 100 100     168 if ($response && ref $response eq 'HASH') {
123 4         11 return [ $self->response($response) ];
124             }
125 7 100 100     50 if (!$response || ref $response ne 'ARRAY') {
126 2         22 return [ 'expect Array or Object' ];
127             }
128 5 100       6 if (!@{$response}) {
  5         15  
129 1         6 return [ 'empty Array' ];
130             }
131              
132 4         6 return map {[ $self->response($_) ]} @{$response};
  5         11  
  4         7  
133             }
134              
135             sub response { ## no critic (ProhibitExcessComplexity RequireArgUnpacking)
136 80     80 1 32626 my ($self, $json) = @_;
137 80 100       255 croak 'require 1 param' if @_ != 2;
138              
139 78         104 undef $@;
140 78 100       194 my $response = ref $json ? $json : eval { decode_json($json) };
  65         426  
141 78 100       209 if ($@) {
142 5         17 return 'Parse error';
143             }
144 73 100       214 if (ref $response ne 'HASH') {
145 3         12 return 'expect Object';
146             }
147 70 100 100     409 if (!defined $response->{jsonrpc} || $response->{jsonrpc} ne '2.0') {
148 11         58 return 'expect {jsonrpc}="2.0"';
149             }
150 59 100 100     403 if (!exists $response->{id} || ref $response->{id} || !defined $response->{id}) {
      100        
151 9         42 return 'expect {id} is scalar';
152             }
153 50 100       169 if (!exists $self->{id}{ $response->{id} }) {
154 4         23 return 'unknown {id}';
155             }
156 46 100 100     249 if (!(exists $response->{result} xor exists $response->{error})) {
157 2         10 return 'expect {result} or {error}';
158             }
159 44 100       107 if (exists $response->{error}) {
160 26         50 my $e = $response->{error};
161 26 100       61 if (ref $e ne 'HASH') {
162 6         28 return 'expect {error} is Object';
163             }
164 20 100 100     182 if (!defined $e->{code} || ref $e->{code} || $e->{code} !~ /\A-?\d+\z/xms) {
      100        
165 8         40 return 'expect {error}{code} is Integer';
166             }
167 12 100 100     62 if (!defined $e->{message} || ref $e->{message}) {
168 6         36 return 'expect {error}{message} is String';
169             }
170             ## no critic (ProhibitMagicNumbers)
171 6 100 100     8 if ((3 == keys %{$e} && !exists $e->{data}) || 3 < keys %{$e}) {
  6   100     40  
  5         21  
172 2         14 return 'only optional key must be {error}{data}';
173             }
174             }
175              
176 22         45 my $id = $response->{id};
177 22         31 push @{ $self->{free_id} }, $id;
  22         54  
178 22         55 my $call = delete $self->{id}{ $id };
179 22 100       64 if ($call) {
180 20         80 $call = delete $self->{call}{ refaddr($call) };
181             }
182 22 100       61 if (!$call) {
183 4         51 return; # call was canceled
184             }
185 18         99 return (undef, $response->{result}, $response->{error}, $call);
186             }
187              
188              
189             1; # Magic true value required at end of module
190             __END__
191              
192             =encoding utf8
193              
194             =head1 NAME
195              
196             JSON::RPC2::Client - Transport-independent json-rpc 2.0 client
197              
198              
199             =head1 SYNOPSIS
200              
201             use JSON::RPC2::Client;
202              
203             $client = JSON::RPC2::Client->new();
204              
205             $json_request = $client->notify('method', @params);
206             $json_request = $client->notify_named('method', %params);
207             ($json_request, $call) = $client->call('method', @params);
208             ($json_request, $call) = $client->call_named('method', %params);
209              
210             ($json_request, @call) = $client->batch(
211             $client->call('method1', @params),
212             $client->call('method2', @params),
213             $client->notify('method', @params),
214             $client->call_named('method', %params),
215             $client->notify_named('method', %params),
216             );
217              
218             $client->cancel($call);
219              
220             ($failed, $result, $error, $call) = $client->response($json_response);
221              
222             for ($client->batch_response($json_response)) {
223             ($failed, $result, $error, $call) = @{ $_ };
224             ...
225             }
226              
227             @call = $client->pending();
228              
229             #
230             # EXAMPLE of simple blocking STDIN-STDOUT client
231             #
232            
233             $client = JSON::RPC2::Client->new();
234             $json_request = $client->call('method', @params);
235              
236             printf "%s\n", $json_request;
237             $json_response = <STDIN>;
238             chomp $json_response;
239              
240             ($failed, $result, $error) = $client->response($json_response);
241             if ($failed) {
242             die "bad response: $failed";
243             } elsif ($error) {
244             printf "method(@params) failed with code=%d: %s\n",
245             $error->{code}, $error->{message};
246             } else {
247             print "method(@params) returned $result\n";
248             }
249              
250             =head1 DESCRIPTION
251              
252             Transport-independent implementation of json-rpc 2.0 client.
253             Can be used both in sync (simple, for blocking I/O) and async
254             (for non-blocking I/O in event-based environment) mode.
255              
256              
257             =head1 INTERFACE
258              
259             =over
260              
261             =item new()
262              
263             Create and return new client object, which can be used to generate requests
264             (notify(), call()), parse responses (responses()) and cancel pending requests
265             (cancel(), pending()).
266              
267             Each client object keep track of request IDs, so you must use dedicated
268             client object for each connection to server.
269              
270             =item notify( $remote_method, @remote_params )
271              
272             =item notify_named( $remote_method, %remote_params )
273              
274             Notifications doesn't receive any replies, so they unreliable.
275              
276             Return ($json_request) - scalar which should be sent to server in any way.
277              
278             =item call( $remote_method, @remote_params )
279              
280             =item call_named( $remote_method, %remote_params )
281              
282             Return ($json_request, $call) - scalar which should be sent to server in
283             any way and identifier of this remote procedure call.
284              
285             The $call is just empty HASHREF, which can be used to: 1) keep user data
286             related to this call in hash fields - that $call will be returned by
287             response() when response to this call will be received; 2) to cancel()
288             this call before response will be received. There usually no need for
289             user to keep $call somewhere unless he wanna be able to cancel() that call.
290              
291             In scalar context return only $json_request - this enough for simple
292             blocking clients which doesn't need to detect which of several pending()
293             calls was just replied or cancel() pending calls.
294              
295             =item batch( $json_request1, $json_request2, $call2, $json_request3, … )
296              
297             Return ($json_request, @call) - scalar which should be sent to server in
298             any way and identifiers of these remote procedure calls (they'll be in
299             same order as they was in params). These two example are equivalent:
300              
301             ($json_request, $call1, $call3) = $client->batch(
302             $client->call('method1'),
303             $client->notify('method2'),
304             $client->call('method3'),
305             );
306              
307             ($json1, $call1) = $client->call('method1');
308             $json2 = $client->notify('method2');
309             ($json3, $call3) = $client->call('method3');
310             $json_request = $client->batch($json1, $json2, $json3);
311              
312             If you're using batch() to send some requests then you should process
313             RPC server's responses using batch_response(), not response().
314              
315             =item batch_response( $json_response )
316              
317             The $json_response can be either JSON string or ARRAYREF/HASHREF (useful
318             with C<< $handle->push_read(json => sub{...}) >> from L<AnyEvent::Handle>).
319              
320             Will parse $json_response and return list with ARRAYREFs, which contain
321             4 elements returned by response().
322              
323             It is safe to always use batch_response() instead of response(), even if
324             you don't send batch() requests at all.
325              
326             =item response( $json_response )
327              
328             The $json_response can be either JSON string or HASHREF (useful
329             with C<< $handle->push_read(json => sub{...}) >> from L<AnyEvent::Handle>).
330              
331             Will parse $json_response and return list with 4 elements:
332              
333             ($failed, $result, $error, $call)
334              
335             $failed parse error message if $json_response is incorrect
336             $result data returned by successful remote method call
337             $error error returned by failed remote method call
338             $call identifier of this call
339              
340             If $failed defined then all others are undefined. Usually that mean either
341             bug in json-rpc client or server.
342              
343             Only one of $result and $error will be defined. Format of $result
344             completely depends on data returned by remote method. $error is HASHREF
345             with fields {code}, {message}, {data} - code should be integer, message
346             should be string, and data is optional value in arbitrary format.
347              
348             The $call should be used to identify which of currently pending() calls
349             just returns - it will be same HASHREF as was initially returned by call()
350             when starting this remote procedure call, and may contain any user data
351             which was placed in it after calling call().
352              
353             There also special case when all 4 values will be undefined - that happens
354             if $json_response was related to call which was already cancel()ed by user.
355              
356             If you're using batch() to send some requests then you should process
357             RPC server's responses using batch_response(), not response().
358              
359             =item cancel( $call )
360              
361             Will cancel that $call. This doesn't affect server - it will continue
362             processing related request and will send response when ready, but that
363             response will be ignored by client's response().
364              
365             Return nothing.
366              
367             =item pending()
368              
369             Return list with all currently pending $call's.
370              
371             =back
372              
373              
374             =head1 DIAGNOSTICS
375              
376             None.
377              
378              
379             =head1 CONFIGURATION AND ENVIRONMENT
380              
381             JSON::RPC2::Client requires no configuration files or environment variables.
382              
383              
384             =head1 DEPENDENCIES
385              
386             JSON::XS
387              
388              
389             =head1 INCOMPATIBILITIES
390              
391             None reported.
392              
393              
394             =head1 BUGS AND LIMITATIONS
395              
396             No bugs have been reported.
397              
398             Please report any bugs or feature requests to
399             C<bug-json-rpc2-client@rt.cpan.org>, or through the web interface at
400             L<http://rt.cpan.org>.
401              
402              
403             =head1 AUTHOR
404              
405             Alex Efros C<< <powerman-asdf@ya.ru> >>
406              
407              
408             =head1 LICENSE AND COPYRIGHT
409              
410             Copyright (c) 2009,2013-2014, Alex Efros C<< <powerman-asdf@ya.ru> >>. All rights reserved.
411              
412             This module is free software; you can redistribute it and/or
413             modify it under the same terms as Perl itself. See L<perlartistic>.
414              
415              
416             =head1 DISCLAIMER OF WARRANTY
417              
418             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
419             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
420             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
421             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
422             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
423             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
424             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
425             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
426             NECESSARY SERVICING, REPAIR, OR CORRECTION.
427              
428             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
429             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
430             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
431             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
432             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
433             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
434             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
435             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
436             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
437             SUCH DAMAGES.