File Coverage

blib/lib/JSON/RPC2/TwoWay/Connection.pm
Criterion Covered Total %
statement 83 109 76.1
branch 42 70 60.0
condition 17 33 51.5
subroutine 16 21 76.1
pod 7 9 77.7
total 165 242 68.1


line stmt bran cond sub pod time code
1             package JSON::RPC2::TwoWay::Connection;
2              
3 2     2   25 use 5.10.0;
  2         7  
4 2     2   10 use strict;
  2         5  
  2         38  
5 2     2   9 use warnings;
  2         4  
  2         97  
6              
7             our $VERSION = '0.07'; # VERSION
8              
9             # standard perl
10 2     2   13 use Carp;
  2         4  
  2         99  
11 2     2   12 use Data::Dumper;
  2         4  
  2         104  
12 2     2   25 use Digest::MD5 qw(md5_base64);
  2         4  
  2         96  
13 2     2   15 use Scalar::Util qw(refaddr weaken);
  2         3  
  2         114  
14              
15             # cpan
16 2     2   18 use JSON::MaybeXS qw();
  2         4  
  2         42  
17              
18 2     2   10 use constant ERR_REQ => -32600;
  2         4  
  2         2768  
19              
20             sub new {
21 2     2 1 11 my ($class, %opt) = @_;
22 2 50 33     16 croak 'no rpc?' unless $opt{rpc} and $opt{rpc}->isa('JSON::RPC2::TwoWay');
23             #croak 'no stream?' unless $opt->{stream} and $opt->{stream}->can('write');
24 2 100 66     101 croak 'no write?' unless $opt{write} and ref $opt{write} eq 'CODE';
25             my $self = {
26             calls => {},
27             debug => $opt{debug} ? 1 : 0,
28 0     0   0 log => ref $opt{debug} eq 'CODE' ? $opt{debug} : sub { say STDERR @_ },
29             next_id => 1,
30             owner => $opt{owner},
31             request => undef,
32             rpc => $opt{rpc},
33             json => $opt{rpc}->{json},
34             state => undef,
35             write => $opt{write},
36 1 50       14 };
    50          
37 1         5 weaken $self->{owner};
38 1         4 return bless $self, $class;
39             }
40              
41             sub call {
42 5     5 1 3314 my ($self, $name, $args, $cb) = @_;
43 5 50       15 croak 'no self?' unless $self;
44 5 100 66     163 croak 'args should be a array or hash reference'
45             unless ref $args eq 'ARRAY' or ref $args eq 'HASH';
46 4 100       83 croak 'no callback?' unless $cb;
47 3 100       80 croak 'callback should be a code reference' if ref $cb ne 'CODE';
48 2         29 my $id = md5_base64($self->{next_id}++ . $name . $self->{json}->encode($args) . refaddr($cb));
49 2 50       9 croak 'duplicate call id' if $self->{calls}->{$id};
50             my $request = $self->{json}->encode({
51 2         17 jsonrpc => '2.0',
52             method => $name,
53             params => $args,
54             id => $id,
55             });
56 2         9 $self->{calls}->{$id} = [ $cb, 0 ]; # not raw
57             #$self->{log}->("call: $request") if $self->{debug};
58 2         7 $self->write($request);
59 2         9 return;
60             }
61              
62             sub callraw {
63 0     0 1 0 my ($self, $request, $cb) = @_;
64 0 0       0 croak 'no self?' unless $self;
65 0 0       0 croak 'request should be a array or hash reference'
66             unless ref $request eq 'HASH';
67 0 0       0 croak 'callback should be a code reference' if ref $cb ne 'CODE';
68 0         0 my $id = md5_base64($self->{next_id}++ . $self->{json}->encode($request) . refaddr($cb));
69 0 0       0 croak 'duplicate call id' if $self->{calls}->{$id};
70 0         0 $request->{jsonrpc} = '2.0';
71 0         0 $request->{id} = $id;
72 0         0 $request = $self->{json}->encode($request);
73 0         0 $self->{calls}->{$id} = [ $cb, 1 ]; # raw
74             #$self->{log}->("callraw: $request") if $self->{debug};
75 0         0 $self->write($request);
76 0         0 return;
77             }
78              
79             sub notify {
80 0     0 1 0 my ($self, $name, $args, $cb) = @_;
81 0 0       0 croak 'no self?' unless $self;
82 0 0 0     0 croak 'args should be a array of hash reference'
83             unless ref $args eq 'ARRAY' or ref $args eq 'HASH';
84             my $request = $self->{json}->encode({
85 0         0 jsonrpc => '2.0',
86             method => $name,
87             params => $args,
88             });
89             #$self->{log}->("notify: $request") if $self->{debug};
90 0         0 $self->write($request);
91 0         0 return;
92             }
93              
94             sub handle {
95 17     17 1 10452 my ($self, $json) = @_;
96 17         49 my @err = $self->_handle(\$json);
97 17 100       66 $self->{rpc}->_error($self, undef, ERR_REQ, 'Invalid Request: ' . $err[0]) if $err[0];
98 17         51 return @err;
99             }
100              
101             sub _handle {
102 17     17   33 my ($self, $jsonr) = @_;
103 17 50       46 $self->{log}->(' handle: ' . $$jsonr) if $self->{debug};
104 17         27 local $@;
105 17         32 my $r = eval { $self->{json}->decode($$jsonr) };
  17         130  
106 17 100       46 return "json decode failed: $@" if $@;
107 16 100       45 return 'not a json object' if ref $r ne 'HASH';
108 15 100 66     68 return 'expected jsonrpc version 2.0' unless defined $r->{jsonrpc} and $r->{jsonrpc} eq '2.0';
109             # id can be null in the error case
110 14 100 100     53 return 'id is not a string or number' if exists $r->{id} and ref $r->{id};
111 13 100 100     48 if (defined $r->{method}) {
    100 33        
112 9         30 return $self->{rpc}->_handle_request($self, $r);
113             } elsif (exists $r->{id} and (exists $r->{result} or defined $r->{error})) {
114 3         8 return $self->_handle_response($r);
115             } else {
116 1         4 return 'invalid jsonnrpc object';
117             }
118             }
119              
120             sub _handle_response {
121 3     3   24 my ($self, $r) = @_;
122             #$self->{log}->('_handle_response: ' . Dumper($r)) if $self->{debug};
123 3         7 my $id = $r->{id};
124 3         7 my ($cb, $raw);
125 3 50       10 $cb = delete $self->{calls}->{$id} if $id;
126 3 100       11 return unless $cb;
127 2         6 ($cb, $raw) = @$cb;
128 2 100       6 if (defined $r->{error}) {
129 1         3 my $e = $r->{error};
130 1 50       4 return 'error is not an object' unless ref $e eq 'HASH';
131 1 50 33     14 return 'error code is not a integer' unless defined $e->{code} and $e->{code} =~ /^-?\d+$/;
132 1 50       9 return 'error message is not a string' if ref $e->{message};
133 1 50 33     10 return 'extra members in error object' if (keys %$e == 3 and !exists $e->{data}) or (keys %$e > 2);
      33        
134 1 50       4 if ($raw) {
135 0         0 $cb->($r);
136             } else {
137 1         3 $cb->($e);
138             }
139             } else {
140 1 50       5 if ($raw) {
141 0         0 $cb->(0, $r);
142             } else {
143 1         23 $cb->(0, $r->{result});
144             }
145             }
146 2         39 return;
147             }
148              
149             sub write {
150 16     16 0 28 my $self = shift;
151 16 50       37 $self->{log}->(' writing: ' . join '', @_) if $self->{debug};
152 16         41 $self->{write}->(@_);
153             }
154              
155             sub owner {
156 0     0 1 0 my $self = shift;
157 0 0       0 weaken ($self->{owner} = shift) if (@_);
158 0         0 return $self->{owner};
159             }
160              
161             sub state {
162 4     4 0 510 my $self = shift;
163 4 100       10 $self->{state} = shift if (@_);
164 4         22 return $self->{state};
165             }
166              
167              
168             sub close {
169 0     0 1   my $self = shift;
170 0           %$self = (); # nuke'm all
171             }
172              
173             #sub DESTROY {
174             # my $self = shift;
175             # $self->{log}->('destroying ' . $self) if $self->{debug};
176             #}
177              
178             1;
179              
180             =encoding utf8
181              
182             =head1 NAME
183              
184             JSON::RPC2::TwoWay::Connection - Transport-independent bidirectional JSON-RPC 2.0 connection
185              
186             =head1 SYNOPSIS
187              
188             $rpc = JSON::RPC2::TwoWay->new();
189             $rpc->register('ping', \&handle_ping);
190              
191             $con = $rpc->newconnection(
192             owner => $owner,
193             write => sub { $stream->write(@_) }
194             );
195             @err = $con->handle($stream->read);
196             die $err[-1] if @err;
197              
198             =head1 DESCRIPTION
199              
200             L is a connection containter for
201             L.
202              
203             =head1 METHODS
204              
205             =head2 new
206              
207             $con = JSON::RPC2::TwoWay::Connection->new(option => ...);
208              
209             Class method that returns a new JSON::RPC2::TwoWay::Connection object.
210             Use newconnection() on a L object instead.
211              
212             Valid arguments are:
213              
214             =over 4
215              
216             =item - debug: print debugging to STDERR, or if coderef is given call that with
217             the debugging line.
218              
219             (default false)
220              
221             =item - owner: 'owner' object of this connection.
222              
223             When provided this object will be asked for the 'state' of the connection.
224             Otherwise state will always be 0.
225              
226             =item - rpc: the L object to handle incoming method calls
227              
228             (required)
229              
230             =item - write: a coderef called for writing
231              
232             This coderef will be called for all output: both requests and responses.
233             (required)
234              
235             =back
236              
237             =head2 call
238              
239             $con->call('method', { arg => 'foo' }, $cb);
240              
241             Calls the remote method indicated in the first argument.
242              
243             The second argument should either be a arrayref or hashref, depending on
244             wether the remote method requires positional of by-name arguments. Pass a
245             empty reference when there are no arguments.
246              
247             The third argument is a callback: this callback will
248             be called with the results of the called method.
249              
250             Call throws an error in case of missing arguments, otherwise it returns
251             immediately with no return value.
252              
253             =head2 callraw
254              
255             $con->callraw({ method => 'method', params => {..} }, $cb);
256              
257             Enhances the first argument (which should be a hashref) to a full JSON-RPC
258             2.0 request object and sends the request. This allows for manipulating and
259             extending the actual request.
260              
261             The third argument is a callback: this callback will
262             be called with the results of the called method.
263              
264             Callraw throws an error in case of missing arguments, otherwise it returns
265             immediately with no return value.
266              
267             =head3 the result callback
268              
269             The result callback is called with 1 or 2 arguments. The first argument is
270             a protocol-error-flag, it contains a error message when there was some kind
271             of protocol error like calling a normal method as a notification.
272              
273             If there are 2 arguments the first one is always false, the second one will
274             contain the results from the remote method, see "REGISTERED CALLBACK CALLING
275             CONVENTION" in "L. The full response will be passed for
276             access to any extra fields.
277              
278             =head2 notify
279              
280             $con->notify('notify_me', { baz => 'foo' })
281              
282             Calls the remote method as a notification, i.e. no response will be
283             expected. Notify throws an error in case of missing arguments, otherwise it
284             returns immediately with no return value.
285              
286             =head2 handle
287              
288             $con->handle($jsonblob)
289              
290             Handle the incoming request or response. Requests (if valid) are passed on
291             to the registered callback for that method. Repsonses (if valid) are passed
292             on to the callback provided in the call.
293              
294             Handle returns 0, 1 or 2 values. If no value is returned there were no
295             errors during processing. If 1 value is returned there was a 'fatal' error,
296             and the value is the error message. If 2 values are returned there was a
297             'normal' error, the first value is false, the second value is the error
298             message.
299              
300             In case of an error, handle will call the provided write callback with a
301             appropriate error response to be sent to the other side. The application
302             using the JSON::RPC2::TwoWay::Connection is advised to close the underlying
303             connection in case of fatal errors.
304              
305             =head2 close
306              
307             $con->close()
308              
309             Closes the connection. Recommended to be used to avoid memory leaks due to
310             circular references.
311              
312             =head2 owner
313              
314             Getter-setter to allow the application to connect the
315             JSON::RPC2::TwoWay::Connection to some internal connection concept.
316              
317             -head2 state
318              
319             Getter-setter for the connection state. Evaluated by JSON::RPC2::TwoWay
320             when a method was registered with a state option.
321              
322             =head1 SEE ALSO
323              
324             =over
325              
326             =item *
327              
328             L
329              
330             =item *
331              
332             L: JSON-RPC 2.0 Specification
333              
334             =back
335              
336             =head1 ACKNOWLEDGEMENT
337              
338             This software has been developed with support from L.
339             In German: Diese Software wurde mit Unterstützung von L entwickelt.
340              
341             =head1 THANKS
342              
343             =over 4
344              
345             =item *
346              
347             'greencoloured' for multiple PRs
348              
349             =back
350              
351             =head1 AUTHORS
352              
353             =over 4
354              
355             =item *
356              
357             Wieger Opmeer
358              
359             =back
360              
361             =head1 COPYRIGHT AND LICENSE
362              
363             This software is copyright (c) 2016-2022 by Wieger Opmeer.
364              
365             This is free software; you can redistribute it and/or modify it under
366             the same terms as the Perl 5 programming language system itself.
367              
368             =cut
369