File Coverage

blib/lib/JSON/RPC2/TwoWay/Connection.pm
Criterion Covered Total %
statement 87 113 76.9
branch 42 70 60.0
condition 17 33 51.5
subroutine 18 23 78.2
pod 7 9 77.7
total 171 248 68.9


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