File Coverage

blib/lib/JSON/RPC2/AnyEvent/Server.pm
Criterion Covered Total %
statement 90 99 90.9
branch 26 38 68.4
condition 11 12 91.6
subroutine 21 21 100.0
pod 3 3 100.0
total 151 173 87.2


line stmt bran cond sub pod time code
1             package JSON::RPC2::AnyEvent::Server;
2 5     5   96422 use 5.010;
  5         24  
  5         228  
3 5     5   30 use strict;
  5         10  
  5         182  
4 5     5   34 use warnings;
  5         9  
  5         322  
5              
6             our $VERSION = "0.02";
7              
8 5     5   5112 use AnyEvent;
  5         18277  
  5         152  
9 5     5   31 use Carp 'croak';
  5         11  
  5         358  
10 5     5   28 use Scalar::Util 'reftype';
  5         6  
  5         469  
11 5     5   234016 use Try::Tiny;
  5         9261  
  5         355  
12              
13 5     5   3125 use JSON::RPC2::AnyEvent::Constants qw(:all);
  5         13  
  5         9346  
14              
15              
16             sub new {
17 4     4 1 80 my $class = shift;
18 4         17 my $self = bless {}, $class;
19 4         18 while ( @_ ) {
20 8         17 my $method = shift;
21 8         13 my $spec = shift;
22 8 100 100     57 if ( (reftype $spec // '') eq 'CODE' ) {
23 5         17 $self->register($method, $spec);
24             } else {
25 3         9 $self->register($method, $spec, shift);
26             }
27             }
28 4         13 $self;
29             }
30              
31             sub dispatch {
32 11     11 1 19179 my $self = shift;
33 11         16 my $json = shift;
34 11         610 my $ret_cv = AE::cv;
35             try{
36 11     11   382 my $type = _check_format($json); # die when $json's format is invalid
37 9         23 my $method = $self->{$json->{method}};
38 9 50       28 unless ( $method ) { # Method not found
39 0         0 $ret_cv->send(_make_error_response($json->{id}, ERR_METHOD_NOT_FOUND, 'Method not found'));
40 0         0 return $ret_cv;
41             }
42 9 50       26 if ( $type eq 'c' ) { # RPC call
43             $method->(AE::cv{
44 9         2998332 my $cv = shift;
45             try{
46 9         389 $ret_cv->send(_make_response($json->{id}, $cv->recv));
47             } catch {
48 0         0 $ret_cv->send(_make_error_response($json->{id}, ERR_SERVER_ERROR, 'Server error', shift));
49 9         108 };
50 9         255 }, $json->{params});
51 9         236 return $ret_cv;
52             } else { # Notification request (no response)
53 0         0 $method->(AE::cv, $json->{params}); # pass dummy cv
54 0         0 return undef;
55             }
56             } catch { # Invalid request
57 2 50   2   153 my $err = _make_error_response((reftype $json eq 'HASH' ? $json->{id} : undef), ERR_INVALID_REQUEST, 'Invalid Request', shift);
58 2         15 $ret_cv->send($err);
59 2         105 return $ret_cv;
60 11         11846 };
61             }
62              
63             sub _check_format {
64             # Returns
65             # "c" : when the value represents rpc call
66             # "n" : when the value represents notification
67             # croak: when the value is in invalid format
68 11     11   19 my $json = shift;
69 11 50       69 reftype $json eq 'HASH' or croak "JSON-RPC request MUST be an Object (hash)";
70             #$json->{jsonrpc} eq "2.0" or croak "Unsupported JSON-RPC version"; # This module supports only JSON-RPC 2.0 spec, but here just ignores this member.
71 11 100 66     293 exists $json->{method} && not ref $json->{method} or croak "`method' MUST be a String value";
72 10 100       30 if ( exists $json->{params} ) {
73 9 100 100     276 (reftype $json->{params} // '') eq 'ARRAY' || (reftype $json->{params} // '') eq 'HASH' or croak "`params' MUST be an array or an object";
      100        
      100        
74             } else {
75 1         4 $json->{params} = [];
76             }
77 9 50       29 return 'n' unless exists $json->{id};
78 9 50       26 not ref $json->{id} or croak "`id' MUST be neighter an array nor an object";
79 9         21 return 'c';
80             }
81              
82             sub _make_response {
83 9     9   96 my ($id, $result) = @_;
84             {
85 9         68 jsonrpc => '2.0',
86             id => $id,
87             result => $result,
88             };
89             }
90              
91             sub _make_error_response {
92 2     2   7 my ($id, $code, $msg, $data) = @_;
93             {
94 2 50       20 jsonrpc => '2.0',
95             id => $id,
96             error => {
97             code => $code,
98             message => "$msg",
99             (defined $data ? (data => $data) : ()),
100             },
101             };
102             }
103              
104              
105             sub register {
106 8     8 1 17 my $self = shift;
107 8         16 my ($method, $spec, $code) = @_;
108 8 100       35 if ( UNIVERSAL::isa($spec, "CODE") ) { # spec is omitted.
109 5         6 $code = $spec;
110 5     5   19 $spec = sub{ $_[0] };
  5         23  
111             } else {
112 3         8 $spec = _parse_argspec($spec);
113 3 50       11 croak "`$code' is not CODE ref" unless UNIVERSAL::isa($code, 'CODE');
114             }
115             $self->{$method} = sub{
116 9     9   70 my ($cv, $params) = @_;
117 9         171 $code->($cv, $spec->($params), $params);
118 8         54 };
119 8         72 $self;
120             }
121              
122             sub _parse_argspec {
123 3     3   7 my $orig = my $spec = shift;
124 3 100       21 if ( $spec =~ s/^\s*\[\s*// ) { # Wants array
    50          
125 1 50       31 croak "Invalid argspec. Unmatched '[' in argspec: $orig" unless $spec =~ s/\s*\]\s*$//;
126 1         7 my @parts = split /\s*,\s*/, $spec;
127             return sub{
128 1     1   2 my $params = shift;
129 1 50       9 return $params if UNIVERSAL::isa($params, 'ARRAY');
130             # Got a hash! Then, convert it to an array!
131 0         0 my $args = [];
132 0         0 push @$args, $params->{$_} foreach @parts;
133 0         0 return $args;
134 1         8 };
135             } elsif ( $spec =~ s/\s*\{\s*// ) { # Wants hash
136 2 50       14 croak "Invalid argspec. Unmatched '{' in argspec: $orig" unless $spec =~ s/\s*\}\s*$//;
137 2         14 my @parts = split /\s*,\s*/, $spec;
138             return sub{
139 3     3   6 my $params = shift;
140 3 100       14 return $params if UNIVERSAL::isa($params, 'HASH');
141             # Got an array! Then, convert it to a hash!
142 2         4 my $args = {};
143 2         8 for ( my $i=0; $i < @parts; $i++ ) {
144 8         30 $args->{$parts[$i]} = $params->[$i];
145             }
146 2         8 return $args;
147 2         13 };
148             } else {
149 0           croak "Invalid argspec. Argspec must be enclosed in [] or {}: $orig";
150             }
151             }
152              
153              
154              
155             1;
156             __END__
157              
158             =encoding utf-8
159              
160             =head1 NAME
161              
162             JSON::RPC2::AnyEvent::Server - Yet-another, transport-independent, asynchronous and simple JSON-RPC 2.0 server
163              
164             =head1 SYNOPSIS
165              
166             use JSON::RPC2::AnyEvent::Server;
167              
168             my $srv = JSON::RPC2::AnyEvent::Server->new(
169             hello => "[family_name, first_name]" => sub{ # This wants an array as its argument.
170             my ($cv, $args) = @_;
171             my ($family, $given) = @$args;
172             do_some_async_task(sub{
173             # Done!
174             $cv->send("Hello, $given $family!");
175             });
176             }
177             );
178              
179             my $cv = $srv->dispatch({
180             jsonrpc => "2.0",
181             id => 1,
182             method => 'hello',
183             params => [qw(Sogoru Kyo Gunner)],
184             });
185             my $res = $cv->recv; # { jsonrpc => "2.0", id => 1, result => "Hello, Kyo Sogoru!" }
186              
187             my $cv = $srv->dispatch({
188             jsonrpc => "2.0",
189             id => 2,
190             method => 'hello',
191             params => { # You can pass hash as well!
192             first_name => 'Ryoko',
193             family_name => 'Kaminagi',
194             position => 'Wizard'
195             }
196             });
197             my $res = $cv->recv; # { jsonrpc => "2.0", id => 2, result => "Hello, Ryoko Kaminagi!" }
198              
199             # You can add method separately.
200             $srv->register(wanthash => '{family_name, first_name}' => sub{
201             my ($cv, $args, $as_is) = @_;
202             $cv->send({args => $args, as_is => $as_is});
203             });
204              
205             # So, how is params translated?
206             my $cv = $srv->dispatch({
207             jsonrpc => "2.0",
208             id => 3,
209             method => 'wanthash',
210             params => [qw(Sogoru Kyo Gunner)],
211             });
212             my $res = $cv->recv;
213             # {
214             # jsonrpc => "2.0",
215             # id => 3,
216             # result => {
217             # args => { family_name => 'Sogoru', first_name => "Kyo" }, # translated to a hash
218             # as_is => ['Sogoru', 'Kyo', 'Gunner'], # original value
219             # },
220             # }
221              
222             my $cv = $srv->dispatch({
223             jsonrpc => "2.0",
224             id => 4,
225             method => 'wanthash',
226             params => {first_name => 'Ryoko', family_name => 'Kaminagi', position => 'Wizard'},
227             });
228             my $res = $cv->recv;
229             # {
230             # jsonrpc => "2.0",
231             # id => 4,
232             # result => {
233             # args => {first_name => 'Ryoko', family_name => 'Kaminagi', position => 'Wizard'}, # passed as-is
234             # as_is => {first_name => 'Ryoko', family_name => 'Kaminagi', position => 'Wizard'},
235             # },
236             # }
237              
238             # For Notification Request, just returns undef.
239             my $cv = $srv->dispatch({
240             jsonrpc => "2.0",
241             method => "hello",
242             params => [qw(Misaki Shizuno)]
243             });
244             not defined $cv; # true
245              
246              
247             =head1 DESCRIPTION
248              
249             JSON::RPC2::AnyEvent::Server provides asynchronous JSON-RPC 2.0 server implementation. This just provides an abstract
250             JSON-RPC layer and you need to combine concrete transport protocol to utilize this module. If you are interested in
251             stream protocol like TCP, refer to L<JSON::RPC2::AnyEvent::Server::Handle>.
252              
253             =head1 THINK SIMPLE
254              
255             JSON::RPC2::AnyEvent considers JSON-RPC as simple as possible. For example, L<JSON::RPC2::Server> abstracts JSON-RPC
256             server as a kind of hash filter. Unlike L<JSON::RPC2::Server> accepts and outputs serialized JSON text,
257             L<JSON::RPC2::AnyEvent::Server> accepts and outputs Perl hash:
258              
259             +----------+
260             | |
261             Inuput | JSON-RPC | Output
262             request ---------->| Server |----------> response
263             (as a hash) | | (as a hash)
264             +----------+
265              
266             This has nothing to do with serializing Perl data or deserializing JSON text!
267              
268             See also L<JSON::RPC2::AnyEvent> for more information.
269              
270              
271             =head1 INTERFACE
272              
273             =head2 C<CLASS-E<gt>new( @args )> -> JSON::RPC2::AnyEvent::Server
274              
275             Create new instance of JSON::RPC2::AnyEvent::Server. Arguments are passed to C<register> method.
276              
277             =head2 C<$server-E<gt>register( $method_name =E<gt> $argspec =E<gt> $callback )> -> C<$self>
278              
279             Registers a subroutine as a JSON-RPC method of C<$server>.
280              
281             =over
282              
283             =item C<$method_name>:Str
284              
285             =item C<$argspec>:Str (optional)
286              
287             =item C<$callback>:CODE
288              
289             =back
290              
291             =head2 C<$server-E<gt>dispatch( $val )> -> (AnyEvent::Condvar | undef)
292              
293             Send C<$val> to C<$server> and execute corresponding method.
294              
295             =over
296              
297             =item C<$val>
298              
299             Any value to send, which looks like JSON data.
300              
301             =back
302              
303              
304             =head1 SEE ALSO
305              
306             =over
307              
308             =item L<JSON::RPC2::AnyEvent>
309              
310             =item L<JSON::RPC2::AnyEvent::Server::Handle>
311              
312             =back
313              
314              
315             =head1 LICENSE
316              
317             Copyright (C) Daisuke (yet another) Maki.
318              
319             This library is free software; you can redistribute it and/or modify
320             it under the same terms as Perl itself.
321              
322             =head1 AUTHOR
323              
324             Daisuke (yet another) Maki E<lt>maki.daisuke@gmail.comE<gt>
325              
326             =cut
327