File Coverage

blib/lib/Mojo/XMLRPC.pm
Criterion Covered Total %
statement 116 123 94.3
branch 75 86 87.2
condition 13 15 86.6
subroutine 18 18 100.0
pod 4 4 100.0
total 226 246 91.8


line stmt bran cond sub pod time code
1             package Mojo::XMLRPC;
2              
3 3     3   893857 use Mojo::Base -strict;
  3         21  
  3         28  
4              
5 3     3   508 use B ();
  3         9  
  3         78  
6 3     3   448 use Mojo::ByteStream;
  3         4806  
  3         165  
7 3     3   493 use Mojo::Date;
  3         4638  
  3         29  
8 3     3   539 use Mojo::DOM;
  3         13354  
  3         103  
9 3     3   516 use Mojo::JSON;
  3         17782  
  3         116  
10 3     3   850 use Mojo::Template;
  3         40338  
  3         27  
11 3     3   99 use Scalar::Util ();
  3         6  
  3         55  
12              
13 3     3   852 use Mojo::XMLRPC::Base64;
  3         6  
  3         13  
14 3     3   828 use Mojo::XMLRPC::Message::Call;
  3         7  
  3         18  
15 3     3   801 use Mojo::XMLRPC::Message::Response;
  3         9  
  3         18  
16              
17 3     3   94 use Exporter 'import';
  3         6  
  3         4155  
18              
19             our @EXPORT_OK = (qw[decode_xmlrpc encode_xmlrpc from_xmlrpc to_xmlrpc]);
20              
21             our $VERSION = '0.05';
22             $VERSION = eval $VERSION;
23              
24             my $message = Mojo::Template->new(
25             auto_escape => 1,
26             name => 'message',
27             namespace => __PACKAGE__,
28             )->parse(<<'TEMPLATE');
29             % my ($tag, $method, @args) = @_;
30            
31             <<%= $tag %>>
32             % if (defined $method) {
33             <%= $method %>
34             % }
35             % for my $arg (@args) {
36             %= $arg
37             % }
38             >
39             TEMPLATE
40              
41             my $fault = Mojo::Template->new(
42             auto_escape => 1,
43             name => 'fault',
44             namespace => __PACKAGE__,
45             )->parse(<<'TEMPLATE');
46             % my ($code, $string) = @_;
47            
48            
49            
50             faultCode
51             <%= $code // '' %>
52            
53            
54             faultString
55             <%= $string // '' %>
56            
57            
58            
59             TEMPLATE
60              
61             my $params = Mojo::Template->new(
62             auto_escape => 1,
63             name => 'params',
64             namespace => __PACKAGE__,
65             )->parse(<<'TEMPLATE');
66             % my (@params) = @_;
67            
68             % for my $param (@params) {
69             <%= $param %>
70             % }
71            
72             TEMPLATE
73              
74 4     4 1 23922 sub decode_xmlrpc { from_xmlrpc(Mojo::Util::decode 'UTF-8', $_[0]) }
75              
76 4     4 1 4041 sub encode_xmlrpc { Mojo::Util::encode 'UTF-8', to_xmlrpc(@_) }
77              
78             sub from_xmlrpc {
79 16     16 1 11512 my ($xml) = shift;
80              
81             # parse the XML document
82 16         79 my $dom = Mojo::DOM->new($xml);
83 16         12565 my $msg;
84              
85             # detect the message type
86 16         51 my $top = $dom->children->first;
87 16         1498 my $type = $top->tag;
88 16 100       228 if ($type eq 'methodCall') {
    50          
89 4         32 $msg = Mojo::XMLRPC::Message::Call->new;
90 4 50       30 if (defined(my $method = $top->children('methodName')->first)) {
91 4         1697 $msg->method_name($method->text);
92             }
93              
94             } elsif ($type eq 'methodResponse') {
95 12         50 $msg = Mojo::XMLRPC::Message::Response->new;
96 12 100       67 if (defined(my $fault = $top->children('fault')->first)) {
97 2         418 return $msg->fault(_decode_element($fault));
98             }
99              
100             } else {
101 0         0 die 'unknown type of message';
102             }
103              
104 14 50       2360 if (defined(my $params = $top->children('params')->first)) {
105 14         3087 $msg->parameters([ map { _decode_element($_) } @{ $params->children('param') } ]);
  14         2454  
  14         61  
106             }
107              
108 14         649 return $msg;
109             }
110              
111             sub to_xmlrpc {
112 41     41 1 106438 my ($type, @args) = @_;
113              
114 41 100 66     236 if (Scalar::Util::blessed($type) && $type->isa('Mojo::XMLRPC::Message')) {
115 3         7 my $obj = $type;
116 3 100       17 if ($obj->isa('Mojo::XMLRPC::Message::Call')) {
    50          
117 1         4 $type = 'call';
118 1         6 @args = ($obj->method_name, @{ $obj->parameters });
  1         14  
119             } elsif ($obj->isa('Mojo::XMLRPC::Message::Response')) {
120 2 100       7 $type = $obj->is_fault ? 'fault' : 'response';
121 2 100       15 @args = $obj->is_fault ? @{ $obj->fault }{qw/faultCode faultString/} : @{ $obj->parameters };
  1         6  
  1         7  
122             } else {
123 0         0 die 'Message type not understood';
124             }
125             }
126              
127 41 100       126 my $tag = $type eq 'call' ? 'methodCall' : 'methodResponse';
128 41 100       107 my $method = $type eq 'call' ? shift @args : undef;
129              
130             my $xml =
131             $type eq 'fault' ? $fault->process(@args) :
132 41 100       153 @args ? $params->process(map { _encode_item($_) } @args) :
  39 100       94  
133             undef;
134              
135 41 100       6693 return $message->process($tag, $method, defined($xml) ? Mojo::ByteStream->new($xml) : ());
136             }
137              
138             sub _decode_element {
139 68     68   3674 my $elem = shift;
140 68         127 my $tag = $elem->tag;
141              
142 68 100 100     1132 if ($tag eq 'param' || $tag eq 'value' || $tag eq 'fault') {
    100 100        
    100 100        
    100 66        
    100          
    50          
    100          
    100          
    100          
    50          
143 43         86 my $children = $elem->children;
144              
145             # elements with no children must be strings
146 43 100       2773 return $elem->text unless @$children;
147              
148             # otherwise they are another element
149 41         89 return _decode_element($children->first);
150              
151             } elsif ($tag eq 'array') {
152 1         4 my $data = $elem->children('data')->first;
153 1         204 return [ map { _decode_element($_) } @{ $data->children('value') } ];
  5         761  
  1         4  
154              
155             } elsif ($tag eq 'struct') {
156             return +{
157             map {;
158 6         981 $_->children('name')->first->text, # key
159             _decode_element($_->children('value')->first) # value
160             }
161 3         6 @{ $elem->children('member') } # pairs
  3         10  
162             };
163              
164             } elsif ($tag eq 'int' || $tag eq 'i4') {
165 9         23 return $elem->text + 0;
166              
167             } elsif ($tag eq 'string' || $tag eq 'name') {
168 5         14 return $elem->text;
169              
170             } elsif ($tag eq 'double') {
171 0         0 return $elem->text / 1.0;
172              
173             } elsif ($tag eq 'boolean') {
174 3 100       9 return $elem->text ? Mojo::JSON::true : Mojo::JSON::false;
175              
176             } elsif ($tag eq 'nil') {
177 1         7 return undef;
178              
179             } elsif ($tag eq 'dateTime.iso8601') {
180 2         7 my $date = Mojo::Date->new($elem->text);
181 2 100       241 unless ($date->epoch) {
182 1         496 require Time::Piece;
183 1         6285 $date->epoch(Time::Piece->strptime($elem->text, '%Y%m%dT%H:%M:%S')->epoch);
184             }
185 2         217 return $date;
186             } elsif ($tag eq 'base64') {
187 1         6 return Mojo::XMLRPC::Base64->new(encoded => $elem->text);
188             }
189              
190 0         0 die "unknown tag: $tag";
191             }
192              
193             sub _encode_item {
194 44     44   77 my $item = shift;
195 44         63 my $ret;
196              
197 44 100       98 if (ref $item) {
198 9 100       53 if (Scalar::Util::blessed $item) {
    100          
    100          
    50          
199 5 50       94 if ($item->can('TO_XMLRPC')) {
    100          
    100          
    50          
    0          
200 0         0 return _encode_item($item->TO_XMLRPC);
201              
202             } elsif ($item->isa('JSON::PP::Boolean')) {
203 2 100       40 my $val = $item ? 1 : 0;
204 2         35 $ret = "$val";
205              
206             } elsif ($item->isa('Mojo::Date')) {
207 1         7 my $date = $item->to_datetime;
208 1         44 $ret = "$date";
209              
210             } elsif ($item->isa('Mojo::XMLRPC::Base64')) {
211 2         46 $ret = "$item";
212              
213             } elsif ($item->can('TO_JSON')) {
214 0         0 return _encode_item($item->TO_JSON);
215              
216             } else {
217 0         0 return _encode_item("$item");
218             }
219             } elsif (ref $item eq 'ARRAY') {
220 1         3 $ret = join '', map { '' . _encode_item($_) . '' } @$item;
  3         23  
221 1         11 $ret = "$ret";
222              
223             } elsif (ref $item eq 'HASH') {
224             $ret = join '', map {
225 1         4 my $name = Mojo::Util::xml_escape($_);
  2         15  
226 2         19 "$name" . _encode_item($item->{$_}) . '';
227             } keys %$item;
228 1         12 $ret = "$ret";
229              
230             } elsif (ref $item eq 'SCALAR') {
231 2 100       10 my $val = $$item ? 1 : 0;
232 2         9 $ret = "$val";
233             }
234             }
235             else {
236 35         123 my $sv = B::svref_2object(\$item);
237              
238 35 100       266 $ret =
    100          
    100          
239             !defined $item ? '' :
240             $sv->FLAGS & B::SVf_NOK ? "$item" :
241             $sv->FLAGS & B::SVf_IOK ? "$item" :
242             '' . Mojo::Util::xml_escape($item) . '';
243             }
244              
245 44         414 return Mojo::ByteStream->new($ret);
246             }
247              
248             1;
249              
250             =encoding utf8
251              
252             =head1 NAME
253              
254             Mojo::XMLRPC - An XMLRPC message parser/encoder using the Mojo stack
255              
256             =head1 SYNOPSIS
257              
258             use Mojo::UserAgent;
259             use Mojo::XMLRPC qw[to_xmlrpc from_xmlrpc];
260              
261             my $ua = Mojo::UserAgent->new;
262             my $url = ...;
263             my $tx = $ua->post($url, encode_xmlrpc(call => 'mymethod', 'myarg'));
264             my $res = decode_xmlrpc($tx->res->body)
265              
266             =head1 DESCRIPTION
267              
268             L is a pure-perl XML-RPC message parser and encoder.
269             It uses tools from the L toolkit to do all of the work.
270              
271             This does not mean that it must only be used in conjunction with a L app, far from it.
272             Feel free to use it in any circumstance that needs XML-RPC messages.
273              
274             =head1 MAPPING
275              
276             The mapping between Perl types and XMLRPC types is not perfectly one-to-one, especially given Perl's scalar types.
277             The following is a description of the procedure used to encode and decode XMLRPC message from/to Perl.
278              
279             =head2 Perl to XMLRPC
280              
281             If the item is a blessed reference:
282              
283             =over
284              
285             =item *
286              
287             If the item/object implements a C method, it is called and the result is encoded.
288              
289             =item *
290              
291             If the item is a L, as the L booleans are, it is encoded as a C.
292              
293             =item *
294              
295             If the item is a L then it is encoded as a C.
296              
297             =item *
298              
299             If the item is a L then it is encode as a C.
300             This wrapper class is used to distinguish a string from a base64 and aid in encoding/decoding.
301              
302             =item
303              
304             If the item/object implements a C method, it is called and the result is encoded.
305              
306             =item
307              
308             If none of the above cases are true, the item is stringified and encoded as a C.
309              
310             =back
311              
312             If the item is an unblessed reference:
313              
314             =over
315              
316             =item *
317              
318             An array reference is encoded as an C.
319              
320             =item *
321              
322             A hash reference is encoded as a C.
323              
324             =item *
325              
326             A scalar reference is encoded as a C depending on the truthiness of the referenced value.
327             This is the standard shortcut seen in JSON modules allowing C<\1> for true and C<\0> for false.
328              
329             =back
330              
331             If the item is a non-reference scalar:
332              
333             =over
334              
335             =item *
336              
337             If the item is undefined it is encoded as C<< >>.
338              
339             =item *
340              
341             If the item has C (it has been used as a floating point number) it is encoded as C.
342              
343             =item *
344              
345             If the item has C (it has been used as an integer (and not a float)) it is encoded as an C.
346              
347             =item *
348              
349             All other values are encoded as C.
350              
351             =back
352              
353             =head2 XMLRPC to Perl
354              
355             Most values decode back into Perl in a manner that would survive a round trip.
356             The exceptions are blessed objects that implement C or C or are stringified.
357             The shortcuts for booleans will round-trip to being L booleans objects.
358              
359             Values encoded as integers will not be truncated via C however no attempt is made to upgrade them to C or C.
360             Values encoded as floating point C will be forcably upgraded to C (by dividing by 1.0).
361             This is so that an integer value encoded as a floating point will round trip, the reverse case isn't as useful and thus isn't handled.
362              
363             =head1 FUNCTIONS
364              
365             =head2 decode_xmlrpc
366              
367             Like L but first decodes from UTF-8 encoded bytes.
368              
369             =head2 encode_xmlrpc
370              
371             Like L but encodes the result to UTF-8 encoded bytes.
372              
373             =head2 from_xmlrpc
374              
375             Takes a character string, interprets it, and returns a L containing the result.
376             If the input is UTF-8 encoded bytes, you can use L instead.
377              
378             =head2 to_xmlrpc
379              
380             Generates an XMLRPC message from data passed to the function.
381             The input may be a L or it could be of the following form.
382              
383             =over
384              
385             =item *
386              
387             A message type, one of C, C, C.
388              
389             =item *
390              
391             If the message type is C, then the method name.
392              
393             =item *
394              
395             If the message is not a C, then all remaining arguments are parameters.
396             If the message is a C, then the fault code followed by the fault string, all remaining arguments are ignored.
397              
398             =back
399              
400             The return value is a character string.
401             To generate UTF-8 encoded bytes, you can use L instead.
402              
403             =head1 THANKS
404              
405             This module was inspired by L written by Sébastien Aperghis-Tramoni.
406              
407             L was a port of that module initially to use the L module rather than L.
408             By the time port to the Mojo stack was complete, the module was entirely rewritten.
409             That said, the algorithm still owes a debt of gratitude to that one.
410              
411             =head1 SOURCE REPOSITORY
412              
413             L
414              
415             =head1 AUTHOR
416              
417             Joel Berger, Ejoel.a.berger@gmail.comE
418              
419             =head1 COPYRIGHT AND LICENSE
420              
421             Copyright (C) 2017 by Joel Berger
422             This library is free software; you can redistribute it and/or modify
423             it under the same terms as Perl itself.