File Coverage

blib/lib/Dancer2/Plugin/RPC/JSONRPC.pm
Criterion Covered Total %
statement 103 105 98.1
branch 28 30 93.3
condition 6 9 66.6
subroutine 16 16 100.0
pod 4 4 100.0
total 157 164 95.7


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::RPC::JSONRPC;
2 9     9   3710092 use Dancer2::Plugin;
  9         62651  
  9         74  
3 9     9   25715 use namespace::autoclean;
  9         55685  
  9         45  
4              
5 9     9   568 use v5.10.1;
  9         32  
6 9     9   49 no if $] >= 5.018, warnings => 'experimental::smartmatch';
  9         20  
  9         77  
7              
8             with 'Dancer2::RPCPlugin';
9             our $VERSION = Dancer2::RPCPlugin->VERSION;
10              
11 9     9   2834 use Dancer2::RPCPlugin::CallbackResult::Factory;
  9         33  
  9         479  
12 9     9   1939 use Dancer2::RPCPlugin::DispatchItem;
  9         23  
  9         244  
13 9     9   1651 use Dancer2::RPCPlugin::DispatchMethodList;
  9         41  
  9         227  
14 9     9   1581 use Dancer2::RPCPlugin::FlattenData;
  9         19  
  9         342  
15              
16 9     9   48 use JSON;
  9         14  
  9         64  
17 9     9   990 use Scalar::Util 'blessed';
  9         19  
  9         7999  
18              
19             plugin_keywords 'jsonrpc';
20              
21             sub jsonrpc {
22 9     9 1 249135 my ($plugin, $endpoint, $config) = @_;
23              
24             my $dispatcher = $plugin->dispatch_builder(
25             $endpoint,
26             $config->{publish},
27             $config->{arguments},
28 9         64 plugin_setting(),
29             )->();
30              
31 9         128 my $lister = Dancer2::RPCPlugin::DispatchMethodList->new();
32             $lister->set_partial(
33             protocol => 'jsonrpc',
34             endpoint => $endpoint,
35 9         28 methods => [ sort keys %{ $dispatcher } ],
  9         74  
36             );
37              
38             my $code_wrapper = $config->{code_wrapper}
39             ? $config->{code_wrapper}
40             : sub {
41 8     8   14 my $code = shift;
42 8         14 my $pkg = shift;
43 8         30 $code->(@_);
44 9 100       69 };
45 9         23 my $callback = $config->{callback};
46              
47 9         74 $plugin->app->log(debug => "Starting handler build: ", $lister);
48             my $jsonrpc_handler = sub {
49 15     15   728171 my ($dsl) = @_;
50 15 100       163 if ($plugin->app->request->content_type ne 'application/json') {
51 1         11 $dsl->pass();
52             }
53 14         205 $dsl->app->log(debug => "[handle_jsonrpc_request] Processing: ", $dsl->app->request->body);
54              
55 14         1460 my @requests = unjson($plugin->app->request->body);
56              
57 14         48 $dsl->app->response->content_type('application/json');
58 14         3197 my @responses;
59 14         39 for my $request (@requests) {
60 15         95 my $method_name = $request->{method};
61 15         51 $dsl->app->log(debug => "[handle_jsonrpc_call($method_name)] ", $request);
62              
63 15 100       1844 if (!exists $dispatcher->{$method_name}) {
64 2         8 $dsl->app->log(warning => "$endpoint/#$method_name not found.");
65             push @responses, jsonrpc_error_response(
66             -32601,
67             "Method '$method_name' not found",
68             $request->{id}
69 2         111 );
70 2         9 next;
71             }
72              
73 13         33 my @method_args = $request->{params};
74 13         27 my Dancer2::RPCPlugin::CallbackResult $continue = eval {
75 13 100       101 $callback
76             ? $callback->($plugin->app->request(), $method_name, @method_args)
77             : callback_success();
78             };
79              
80 13 100       310 if (my $error = $@) {
81             push @responses, jsonrpc_error_response(
82             500,
83             $error,
84             $request->{id}
85 1         5 );
86 1         5 next;
87             }
88 12 50 33     206 if (!blessed($continue) || !$continue->isa('Dancer2::RPCPlugin::CallbackResult')) {
    100 66        
89             push @responses, jsonrpc_error_response(
90             -32603,
91             "Internal error: 'callback_result' wrong class " . blessed($continue),
92             $request->{id},
93 0         0 );
94 0         0 next;
95             }
96             elsif (blessed($continue) && !$continue->success) {
97             push @responses, jsonrpc_error_response(
98             $continue->error_code,
99             $continue->error_message,
100             $request->{id},
101 1         6 );
102 1         9 next;
103             }
104              
105 11         35 my Dancer2::RPCPlugin::DispatchItem $di = $dispatcher->{$method_name};
106 11         33 my $handler = $di->code;
107 11         36 my $package = $di->package;
108              
109 11         20 my $result = eval {
110 11         36 $code_wrapper->($handler, $package, $method_name, @method_args);
111             };
112              
113 11         1001 $dsl->app->log(debug => "[handling_jsonrpc_call_response] ", $result);
114 11 100       1540 if (my $error = $@) {
115             push @responses, jsonrpc_error_response(
116             500,
117             $error,
118             $request->{id}
119 1         6 );
120 1         10 next;
121             }
122              
123 10 100 100     68 if (blessed($result) && $result->can('as_jsonrpc_error')) {
124 1         4 my $jsonrpc_error = $result->as_jsonrpc_error;
125             push @responses, jsonrpc_error_response(
126             $jsonrpc_error->{error}{code},
127             $jsonrpc_error->{error}{message},
128             $request->{id}
129 1         6 );
130             }
131             else {
132 9 100       27 if (blessed($result)) {
133 1         5 $result = flatten_data($result);
134             }
135              
136 9         34 push @responses, jsonrpc_response($request->{id}, $result);
137             }
138 10         33 $dsl->app->log(debug => "[pushed_response($method_name)]: ", $responses[-1]);
139             }
140              
141             # create response
142 14         1396 my $response;
143 14 100       53 if (@responses == 1) {
144 13 100       90 if (!defined $responses[0]->{id}) {
145 2         34 $plugin->app->response->status('accepted');
146             }
147             else {
148 11         93 $response = encode_json($responses[0]);
149             }
150             }
151             else {
152 1         4 $response = encode_json([grep {defined($_->{id})} @responses]);
  2         14  
153             }
154              
155 14         246 return $response;
156 9         1446 };
157              
158 9         75 $plugin->app->log(debug => "setting route (jsonrpc): $endpoint ", $lister);
159 9         1221 $plugin->app->add_route(
160             method => 'post',
161             regexp => $endpoint,
162             code => $jsonrpc_handler,
163             );
164 9         23575 return $plugin;
165             }
166              
167             sub unjson {
168 14     14 1 79 my ($body) = @_;
169 14 50       44 return if !$body;
170              
171 14         30 my @requests;
172 14         136 my $unjson = decode_json($body);
173 14 100       59 if (ref($unjson) ne 'ARRAY') {
174 13         44 @requests = ($unjson);
175             }
176             else {
177 1         4 @requests = @$unjson;
178             }
179 14         44 return @requests;
180             }
181              
182             sub jsonrpc_response {
183 9     9 1 25 my ($id, $data) = @_;
184              
185             return {
186 9         48 jsonrpc => '2.0',
187             id => $id,
188             result => $data,
189             };
190             }
191              
192             sub jsonrpc_error_response {
193 6     6 1 19 my ($code, $message, $id) = @_;
194             return {
195 6 100       77 jsonrpc => '2.0',
196             error => {
197             code => $code,
198             message => $message,
199             },
200             defined $id ? (id => $id) : (),
201             };
202             }
203              
204             1;
205              
206             __END__
207              
208             =head1 NAME
209              
210             Dancer2::Plugin::RPC::JSON - Dancer Plugin to register jsonrpc2 methods.
211              
212             =head1 SYNOPSIS
213              
214             In the Controler-bit:
215              
216             use Dancer2::Plugin::RPC::JSON;
217             jsonrpc '/endpoint' => {
218             publish => 'pod',
219             arguments => ['MyProject::Admin']
220             };
221              
222             and in the Model-bit (B<MyProject::Admin>):
223              
224             package MyProject::Admin;
225            
226             =for jsonrpc rpc.abilities rpc_show_abilities
227            
228             =cut
229            
230             sub rpc_show_abilities {
231             return {
232             # datastructure
233             };
234             }
235             1;
236              
237              
238             =head1 DESCRIPTION
239              
240             This plugin lets one bind an endpoint to a set of modules with the new B<jsonrpc> keyword.
241              
242             =head2 jsonrpc '/endpoint' => \%publisher_arguments;
243              
244             =head3 C<\%publisher_arguments>
245              
246             =over
247              
248             =item callback => $coderef [optional]
249              
250             The callback will be called just before the actual rpc-code is called from the
251             dispatch table. The arguments are positional: (full_request, method_name).
252              
253             my Dancer2::RPCPlugin::CallbackResult $continue = $callback
254             ? $callback->(request(), $method_name, @method_args)
255             : callback_success();
256              
257             The callback should return a L<Dancer2::RPCPlugin::CallbackResult> instance:
258              
259             =over 8
260              
261             =item * on_success
262              
263             callback_success()
264              
265             =item * on_failure
266              
267             callback_fail(
268             error_code => <numeric_code>,
269             error_message => <error message>
270             )
271              
272             =back
273              
274             =item code_wrapper => $coderef [optional]
275              
276             The codewrapper will be called with these positional arguments:
277              
278             =over 8
279              
280             =item 1. $call_coderef
281              
282             =item 2. $package (where $call_coderef is)
283              
284             =item 3. $method_name
285              
286             =item 4. @arguments
287              
288             =back
289              
290             The default code_wrapper-sub is:
291              
292             sub {
293             my $code = shift;
294             my $pkg = shift;
295             $code->(@_);
296             };
297              
298             =item publisher => <config | pod | \&code_ref>
299              
300             The publiser key determines the way one connects the rpc-method name with the actual code.
301              
302             =over
303              
304             =item publisher => 'config'
305              
306             This way of publishing requires you to create a dispatch-table in the app's config YAML:
307              
308             plugins:
309             "RPC::JSON":
310             '/endpoint':
311             'MyProject::Admin':
312             admin.someFunction: rpc_admin_some_function_name
313             'MyProject::User':
314             user.otherFunction: rpc_user_other_function_name
315              
316             The Config-publisher doesn't use the C<arguments> value of the C<%publisher_arguments> hash.
317              
318             =item publisher => 'pod'
319              
320             This way of publishing enables one to use a special POD directive C<=for jsonrpc>
321             to connect the rpc-method name to the actual code. The directive must be in the
322             same file as where the code resides.
323              
324             =for jsonrpc admin.someFunction rpc_admin_some_function_name
325              
326             The POD-publisher needs the C<arguments> value to be an arrayref with package names in it.
327              
328             =item publisher => \&code_ref
329              
330             This way of publishing requires you to write your own way of building the dispatch-table.
331             The code_ref you supply, gets the C<arguments> value of the C<%publisher_arguments> hash.
332              
333             A dispatch-table looks like:
334              
335             return {
336             'admin.someFuncion' => dispatch_item(
337             package => 'MyProject::Admin',
338             code => MyProject::Admin->can('rpc_admin_some_function_name'),
339             ),
340             'user.otherFunction' => dispatch_item(
341             package => 'MyProject::User',
342             code => MyProject::User->can('rpc_user_other_function_name'),
343             ),
344             }
345              
346             =back
347              
348             =item arguments => <anything>
349              
350             The value of this key depends on the publisher-method chosen.
351              
352             =back
353              
354             =head2 =for jsonrpc jsonrpc-method-name sub-name
355              
356             This special POD-construct is used for coupling the jsonrpc-methodname to the
357             actual sub-name in the current package.
358              
359             =head1 INTERNAL
360              
361             =head2 unjson
362              
363             Deserializes the string as Perl-datastructure.
364              
365             =head2 jsonrpc_response
366              
367             Returns a jsonrpc response as a hashref.
368              
369             =head2 jsonrpc_error_response
370              
371             Returns a jsonrpc error response as a hashref.
372              
373             =head2 build_dispatcher_from_config
374              
375             Creates a (partial) dispatch table from data passed from the (YAML)-config file.
376              
377             =head2 build_dispatcher_from_pod
378              
379             Creates a (partial) dispatch table from data provided in POD.
380              
381             =head1 COPYRIGHT
382              
383             (c) MMXVI - Abe Timmerman <abeltje@cpan.org>.
384              
385             =cut