File Coverage

blib/lib/Dancer/Plugin/RPC/JSONRPC.pm
Criterion Covered Total %
statement 114 114 100.0
branch 26 26 100.0
condition 9 11 81.8
subroutine 19 19 100.0
pod 5 5 100.0
total 173 175 98.8


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