File Coverage

blib/lib/Dancer2/Plugin/RPC/XMLRPC.pm
Criterion Covered Total %
statement 51 92 55.4
branch 4 22 18.1
condition 0 12 0.0
subroutine 13 15 86.6
pod 2 2 100.0
total 70 143 48.9


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