1 module xmlrpcc.decoder;
2 
3 import std.stdio : writeln;
4 import std.xml : Element, Document;
5 import std.conv : to;
6 import std.exception : enforce;
7 import std.variant : Variant;
8 import std.typecons : Rebindable;
9 import std.string : format, strip;
10 import std.array : replace;
11 import std.datetime : DateTime;
12 import std.base64 : Base64;
13 
14 import xmlrpcc.error;
15 import xmlrpcc.data;
16 
17 @trusted:
18 
19 class DecoderException : XmlRpcException {
20    private this(string msg, string file = __FILE__, size_t line = __LINE__) {
21       super(msg, file, line, next);
22    }
23 }
24 
25 package:
26 
27 MethodCallData decodeCall(string text) {
28    const call = new Document(text);
29    MethodCallData result;
30    result.name = find(call, "methodName").text();
31    result.params = decodeParams(tryFind(call, "params"));
32    return result;
33 }
34 
35 MethodResponseData decodeResponse(string text) {
36    MethodResponseData result;
37    const resp = new Document(text);
38 
39    const fault = tryFind(resp, "fault");
40    if (fault) {
41       result.fault = true;
42       result.params = decodeFault(fault);
43    } else {
44       result.fault = false;
45       result.params = decodeParams(tryFind(resp, "params"));
46    }
47    return result;
48 }
49 
50 private:
51 
52 auto decodeParams(in Element params) {
53    Variant[] result;
54    if (params is null) // Parameters are optional
55       return result;
56 
57    foreach (param; params.elements) {
58       if (param.tag.name != "param") {
59          debug (xmlrpc)
60             writeln("Decoder: Invalid parameter tag: ", param.tag.name);
61          continue;
62       }
63       debug (xmlrpc) {
64          if (param.elements.length != 1)
65             writeln("Decoder: Parameter must contain exactly one sub-element, not ", param.elements.length);
66       }
67       result ~= decodeParam(param);
68    }
69    return result;
70 }
71 
72 auto decodeFault(in Element fault) {
73    Variant[] result;
74    auto value = tryFind(fault, "value");
75    if (value)
76       result ~= decodeValue(value);
77    return result;
78 }
79 
80 auto decodeParam(in Element param) {
81    return decodeValue(find(param, "value"));
82 }
83 
84 Variant decodeValue(in Element value) {
85    Rebindable!(const Element) storage;
86    string type;
87    if (value.elements.length == 0) // Default is string
88    {
89       storage = value;
90       type = "string";
91    } else {
92       enforce(value.elements.length == 1, new DecoderException("Value tag must have at most one sub-element"));
93       storage = value.elements[0];
94       type = storage.tag.name;
95    }
96 
97    if (type == "struct")
98       return decodeStructValue(storage);
99 
100    if (type == "array")
101       return decodeArrayValue(storage);
102 
103    return decodePrimitiveValue(storage, type);
104 }
105 
106 Variant decodeStructValue(in Element storage) {
107    Variant[string] result;
108    foreach (member; storage.elements) {
109       if (member.tag.name != "member") {
110          debug (xmlrpc)
111             writeln("Decoder: Invalid struct subelement: ", member.tag.name);
112          continue;
113       }
114       const nameNode = find(member, "name");
115       const valueNode = find(member, "value");
116       const key = nameNode.text();
117       Variant value = decodeValue(valueNode);
118       result[key] = value;
119    }
120    return Variant(result);
121 }
122 
123 Variant decodeArrayValue(in Element storage) {
124    Variant[] result;
125    const data = find(storage, "data");
126    foreach (value; data.elements) {
127       if (value.tag.name != "value") {
128          debug (xmlrpc)
129             writeln("Decoder: Invalid array member tag: ", value.tag.name);
130          continue;
131       }
132       result ~= decodeValue(value);
133    }
134    return Variant(result);
135 }
136 
137 Variant decodePrimitiveValue(in Element storage, string type) {
138    string data = storage.text();
139    switch (type) {
140    case "int":
141    case "i4":
142       return Variant(to!int(data));
143 
144    case "i8":
145       return Variant(to!long(data));
146 
147    case "string":
148       return Variant(data);
149 
150    case "double":
151       return Variant(to!double(data));
152 
153    case "boolean":
154       debug (xmlrpc) {
155          if (data != "0" && data != "1")
156             writeln("Decoder: Invalid literal for boolean: " ~ data);
157       }
158       return Variant(to!int(data) != 0); // Sloppy conversion
159 
160    case "dateTime.iso8601":
161       data = replace(data, ":", ""); // Conversion to the basic format
162       data = replace(data, "-", "");
163       return Variant(DateTime.fromISOString(data));
164 
165    case "base64":
166       return Variant(Base64.decode(data));
167 
168    case "nil":
169       return Variant(null);
170 
171    default:
172       throw new DecoderException("Unknown XMLRPC type " ~ type);
173    }
174 }
175 
176 const(Element) tryFind(in Element e, string tag) {
177    foreach (sub; e.elements)
178       if (sub.tag.name == tag)
179          return sub;
180    return null;
181 }
182 
183 const(Element) find(in Element e, string tag) {
184    const res = tryFind(e, tag);
185    enforce(res !is null, new DecoderException(format("Element <%s> does not contain <%s>", e.tag.name, tag)));
186    return res;
187 }
188 
189 version (xmlrpc_unittest) unittest {
190    /*
191      * Call
192      */
193    auto s = `<methodCall>
194   <methodName>examples.getStateName</methodName>
195   <params>
196     <param>
197         <value><i4>40</i4></value>
198     </param>
199     <param>
200         <value>South Dakota</value>
201     </param>
202   </params>
203 </methodCall>`;
204    auto call = decodeCall(s);
205    assert(call.name == "examples.getStateName");
206    assert(call.params[0] == 40);
207    assert(call.params[1] == "South Dakota");
208 
209    /*
210      * OK response
211      */
212    s = `<methodResponse>
213   <params>
214     <param>
215         <value>
216           <array>
217             <data>
218               <value><i4>12</i4></value>
219               <value><string>Egypt</string></value>
220               <value>Egypt</value>
221               <value><boolean>0</boolean></value>
222               <value><i4>-31</i4></value>
223             </data>
224           </array>
225         </value>
226     </param>
227   </params>
228 </methodResponse>`;
229    auto resp = decodeResponse(s);
230    assert(!resp.fault);
231    assert(resp.params[0][0] == 12);
232    assert(resp.params[0][1] == "Egypt");
233    assert(resp.params[0][2] == "Egypt");
234    assert(resp.params[0][3] == false);
235    assert(resp.params[0][4] == -31);
236 
237    /*
238      * OK response
239      */
240    s = `<methodResponse>
241   <params>
242     <param>
243         <value>40</value>
244     </param>
245     <param>
246         <value><string>South Dakota</string></value>
247     </param>
248   </params>
249 </methodResponse>`;
250    resp = decodeResponse(s);
251    assert(!resp.fault);
252    assert(resp.params[0] == "40");
253    assert(resp.params[1] == "South Dakota");
254 
255    /*
256      * Fault response
257      */
258    s = `<methodResponse>
259    <fault>
260       <value>
261          <struct>
262             <member>
263                <name>faultCode</name>
264                <value><int>4</int></value>
265             </member>
266             <member>
267                <name>faultString</name>
268                <value><string>Negative, Cassie. Computer control.</string></value>
269             </member>
270          </struct>
271       </value>
272    </fault>
273 </methodResponse>`;
274    resp = decodeResponse(s);
275    assert(resp.fault);
276    assert(resp.params[0]["faultCode"] == 4);
277    assert(resp.params[0]["faultString"] == "Negative, Cassie. Computer control.");
278 
279    /*
280      * Call
281      */
282    s = `<methodCall>
283   <methodName>theMethod</methodName>
284   <params>
285     <param>
286       <value>
287          <struct>
288             <member><name>arrays</name>
289             <value>
290                <array>
291                   <data>
292                      <value><array><data><value><int>10</int></value></data></array></value>
293                      <value><array><data><value><int>15</int></value></data></array></value>
294                   </data>
295                </array>
296             </value>
297             </member>
298             <member><name>question</name><value>Kaneda, what do you see? Kaneda!</value></member>
299          </struct>
300       </value>
301     </param>
302     <param><value><dateTime.iso8601>19980717T14:08:55</dateTime.iso8601></value></param>
303   </params>
304 </methodCall>`;
305    call = decodeCall(s);
306    assert(call.name == "theMethod");
307    Variant arrays = call.params[0]["arrays"];
308    assert(arrays[0][0] == 10);
309    assert(arrays[1][0] == 15);
310    assert(call.params[0]["question"] == "Kaneda, what do you see? Kaneda!");
311    assert(call.params[1] == DateTime(1998, 7, 17, 14, 8, 55));
312 }