File Coverage

blib/lib/Dancer2/Plugin/RPC/RESTRPC.pm
Criterion Covered Total %
statement 78 78 100.0
branch 20 20 100.0
condition 3 3 100.0
subroutine 14 14 100.0
pod 1 1 100.0
total 116 116 100.0


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