File Coverage

blib/lib/Dancer/Plugin/RPC/RESTRPC.pm
Criterion Covered Total %
statement 92 94 97.8
branch 23 26 88.4
condition 9 11 81.8
subroutine 17 17 100.0
pod 2 2 100.0
total 143 150 95.3


line stmt bran cond sub pod time code
1             package Dancer::Plugin::RPC::RESTRPC;
2 5     5   478297 use v5.10;
  5         29  
3 5     5   24 use Dancer ':syntax';
  5         10  
  5         23  
4 5     5   1871 use Dancer::Plugin;
  5         2258  
  5         308  
5 5     5   33 use Scalar::Util 'blessed';
  5         10  
  5         259  
6              
7 5     5   822 no if $] >= 5.018, warnings => 'experimental::smartmatch';
  5         28  
  5         34  
8              
9 5     5   783 use Dancer::RPCPlugin::CallbackResult;
  5         11  
  5         235  
10 5     5   1521 use Dancer::RPCPlugin::ErrorResponse;
  5         13  
  5         206  
11 5     5   481 use Dancer::RPCPlugin::DispatchFromConfig;
  5         11  
  5         188  
12 5     5   559 use Dancer::RPCPlugin::DispatchFromPod;
  5         16  
  5         235  
13 5     5   35 use Dancer::RPCPlugin::DispatchItem;
  5         11  
  5         165  
14 5     5   845 use Dancer::RPCPlugin::DispatchMethodList;
  5         33  
  5         173  
15 5     5   523 use Dancer::RPCPlugin::FlattenData;
  5         12  
  5         3425  
16              
17             my %dispatch_builder_map = (
18             pod => \&build_dispatcher_from_pod,
19             config => \&build_dispatcher_from_config,
20             );
21              
22             register restrpc => sub {
23 13     13   32057 my($self, $base_url, $arguments) = plugin_args(@_);
24              
25 13         71 my $publisher;
26 13   100     56 given ($arguments->{publish} // 'config') {
27 13         40 when (exists $dispatch_builder_map{$_}) {
28 4         27 $publisher = $dispatch_builder_map{$_};
29              
30 4 100       23 $arguments->{arguments} = plugin_setting() if $_ eq 'config';
31             }
32 9         12 default {
33 9         24 $publisher = $_;
34             }
35             }
36 13         79 my $dispatcher = $publisher->($arguments->{arguments}, $base_url);
37              
38 13         120 my $lister = Dancer::RPCPlugin::DispatchMethodList->new();
39             $lister->set_partial(
40             protocol => 'restrpc',
41             endpoint => $base_url,
42 13         35 methods => [ sort keys %{ $dispatcher } ],
  13         83  
43             );
44              
45             my $code_wrapper = $arguments->{code_wrapper}
46             ? $arguments->{code_wrapper}
47             : sub {
48 5     5   10 my $code = shift;
49 5         7 my $pkg = shift;
50 5         22 $code->(@_);
51 13 100       89 };
52 13         33 my $callback = $arguments->{callback};
53              
54 13         53 debug("Starting restrpc-handler build: ", $lister);
55             my $handle_call = sub {
56 11 100   11   70918 if (request->content_type ne 'application/json') {
57 1         16 pass();
58             }
59 10         122 debug("[handle_restrpc_request] Processing: ", request->body);
60              
61             # method_name should exist
62 10         1529 my ($method_name) = request->path =~ m{$base_url/(\w+)};
63 10 50       266 if (! exists $dispatcher->{$method_name}) {
64 0         0 warning("$base_url/#$method_name not found, pass()");
65 0         0 pass();
66             }
67              
68 10         35 content_type 'application/json';
69 10         827 my $response;
70 10 50       47 my $method_args = request->body
71             ? from_json(request->body)
72             : undef;
73 10         837 my Dancer::RPCPlugin::CallbackResult $continue = eval {
74 10 100       51 $callback
75             ? $callback->(request(), $method_name, $method_args)
76             : callback_success();
77             };
78              
79 10 100 66     212 if (my $error = $@) {
    100 66        
    100          
80 1         8 $response = Dancer::RPCPlugin::ErrorResponse->new(
81             error_code => 500,
82             error_message => $error,
83             )->as_restrpc_error;
84             }
85             elsif (!blessed($continue) || !$continue->isa('Dancer::RPCPlugin::CallbackResult')) {
86 1         9 $response = Dancer::RPCPlugin::ErrorResponse->new(
87             error_code => 500,
88             error_message => "Internal error: 'callback_result' wrong class " . blessed($continue),
89             )->as_restrpc_error;
90             }
91             elsif (blessed($continue) && !$continue->success) {
92 1         9 $response = Dancer::RPCPlugin::ErrorResponse->new(
93             error_code => $continue->error_code,
94             error_message => $continue->error_message,
95             )->as_restrpc_error;
96             }
97             else {
98 7         19 my Dancer::RPCPlugin::DispatchItem $di = $dispatcher->{$method_name};
99 7         33 my $handler = $di->code;
100 7         23 my $package = $di->package;
101              
102 7         13 $response = eval {
103 7         21 $code_wrapper->($handler, $package, $method_name, $method_args);
104             };
105              
106 7         63 debug("[handling_jsonrpc_response($method_name)] ", $response);
107 7 100       717 if (my $error = $@) {
108 2         14 $response = Dancer::RPCPlugin::ErrorResponse->new(
109             error_code => 500,
110             error_message => $error,
111             )->as_restrpc_error;
112             }
113 7 100 100     62 if (blessed($response) && $response->can('as_restrpc_error')) {
    100          
114 1         4 $response = $response->as_restrpc_error;
115             }
116             elsif (blessed($response)) {
117 1         6 $response = flatten_data($response);
118             }
119             }
120 10 50       38 $response = { result => $response } if !ref($response);
121 10         33 return to_json($response);
122 13         1769 };
123              
124 13         67 debug("setting routes (restrpc): $base_url ", $lister);
125 13         1224 for my $call (keys %{ $dispatcher }) {
  13         44  
126 18         1446 my $endpoint = "$base_url/$call";
127 18         57 post $endpoint, $handle_call;
128             }
129             };
130              
131             sub build_dispatcher_from_pod {
132 3     3 1 10 my ($pkgs, $endpoint) = @_;
133 3         12 debug("[build_dispatcher_from_pod]");
134 3         29 return dispatch_table_from_pod(
135             plugin => 'restrpc',
136             packages => $pkgs,
137             endpoint => $endpoint,
138             );
139             }
140              
141             sub build_dispatcher_from_config {
142 1     1 1 4 my ($config, $endpoint) = @_;
143 1         7 debug("[build_dispatcher_from_config] ");
144              
145 1         116 return dispatch_table_from_config(
146             plugin => 'restrpc',
147             config => $config,
148             endpoint => $endpoint,
149             );
150             }
151              
152             register_plugin();
153             true;
154              
155             =head1 NAME
156              
157             Dancer::Plugin::RPC::RESTRPC - RESTRPC Plugin for Dancer
158              
159             =head2 SYNOPSIS
160              
161             In the Controler-bit:
162              
163             use Dancer::Plugin::RPC::RESTRPC;
164             restrpc '/base_url' => {
165             publish => 'pod',
166             arguments => ['MyProject::Admin']
167             };
168              
169             and in the Model-bit (B<MyProject::Admin>):
170              
171             package MyProject::Admin;
172            
173             =for restrpc rpc_abilities rpc_show_abilities
174            
175             =cut
176            
177             sub rpc_show_abilities {
178             return {
179             # datastructure
180             };
181             }
182             1;
183              
184             =head1 DESCRIPTION
185              
186             RESTRPC is a simple protocol that uses HTTP-POST to post a JSON-string (with
187             C<Content-Type: application/json> to an endpoint. This endpoint is the
188             C<base_url> concatenated with the rpc-method name.
189              
190             This plugin lets one bind a base_url to a set of modules with the new B<restrpc> keyword.
191              
192             =head2 restrpc '/base_url' => \%publisher_arguments;
193              
194             =head3 C<\%publisher_arguments>
195              
196             =over
197              
198             =item callback => $coderef [optional]
199              
200             The callback will be called just before the actual rpc-code is called from the
201             dispatch table. The arguments are positional: (full_request, method_name).
202              
203             my Dancer::RPCPlugin::CallbackResult $continue = $callback
204             ? $callback->(request(), $method_name, @method_args)
205             : callback_success();
206              
207             The callback should return a L<Dancer::RPCPlugin::CallbackResult> instance:
208              
209             =over 8
210              
211             =item * on_success
212              
213             callback_success()
214              
215             =item * on_failure
216              
217             callback_fail(
218             error_code => <numeric_code>,
219             error_message => <error message>
220             )
221              
222             =back
223              
224             =item code_wrapper => $coderef [optional]
225              
226             The codewrapper will be called with these positional arguments:
227              
228             =over 8
229              
230             =item 1. $call_coderef
231              
232             =item 2. $package (where $call_coderef is)
233              
234             =item 3. $method_name
235              
236             =item 4. @arguments
237              
238             =back
239              
240             The default code_wrapper-sub is:
241              
242             sub {
243             my $code = shift;
244             my $pkg = shift;
245             my $method = shift;
246             $code->(@_);
247             };
248              
249             =item publisher => <config | pod | \&code_ref>
250              
251             The publiser key determines the way one connects the rpc-method name with the actual code.
252              
253             =over
254              
255             =item publisher => 'config'
256              
257             This way of publishing requires you to create a dispatch-table in the app's config YAML:
258              
259             plugins:
260             "RPC::RESTRPC":
261             '/base_url':
262             'MyProject::Admin':
263             admin.someFunction: rpc_admin_some_function_name
264             'MyProject::User':
265             user.otherFunction: rpc_user_other_function_name
266              
267             The Config-publisher doesn't use the C<arguments> value of the C<%publisher_arguments> hash.
268              
269             =item publisher => 'pod'
270              
271             This way of publishing enables one to use a special POD directive C<=for restrpc>
272             to connect the rpc-method name to the actual code. The directive must be in the
273             same file as where the code resides.
274              
275             =for restrpc admin_someFunction rpc_admin_some_function_name
276              
277             The POD-publisher needs the C<arguments> value to be an arrayref with package names in it.
278              
279             =item publisher => \&code_ref
280              
281             This way of publishing requires you to write your own way of building the dispatch-table.
282             The code_ref you supply, gets the C<arguments> value of the C<%publisher_arguments> hash.
283              
284             A dispatch-table looks like:
285              
286             return {
287             'admin_someFuncion' => dispatch_item(
288             package => 'MyProject::Admin',
289             code => MyProject::Admin->can('rpc_admin_some_function_name'),
290             ),
291             'user_otherFunction' => dispatch_item(
292             package => 'MyProject::User',
293             code => MyProject::User->can('rpc_user_other_function_name'),
294             ),
295             }
296              
297             =back
298              
299             =item arguments => <anything>
300              
301             The value of this key depends on the publisher-method chosen.
302              
303             =back
304              
305             =head2 =for restrpc restrpc-method-name sub-name
306              
307             This special POD-construct is used for coupling the restrpc-methodname to the
308             actual sub-name in the current package.
309              
310             =head1 INTERNAL
311              
312             =head2 build_dispatcher_from_config
313              
314             Creates a (partial) dispatch table from data passed from the (YAML)-config file.
315              
316             =head2 build_dispatcher_from_pod
317              
318             Creates a (partial) dispatch table from data provided in POD.
319              
320             =head1 COPYRIGHT
321              
322             (c) MMXVII - Abe Timmerman <abeltje@cpan.org>
323              
324             =cut