File Coverage

blib/lib/Mojo/XMLRPC.pm
Criterion Covered Total %
statement 114 121 94.2
branch 73 84 86.9
condition 13 15 86.6
subroutine 18 18 100.0
pod 4 4 100.0
total 222 242 91.7


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