1 module xmlrpcc.encoder;
2 
3 import std.base64 : Base64;
4 import std.conv : to;
5 import std.datetime : DateTime;
6 import std.exception : enforce;
7 import std.format : format;
8 import std.stdio : writeln;
9 import std.string : join;
10 import std.variant : Variant;
11 import std.xml : Element, Document;
12 
13 import xmlrpcc.error;
14 import xmlrpcc.data;
15 
16 @trusted:
17 
18 class EncoderException : XmlRpcException {
19    private this(string msg, string file = __FILE__, size_t line = __LINE__) {
20       super(msg, file, line, next);
21    }
22 }
23 
24 package:
25 
26 string encodeCall(MethodCallData call) {
27    auto root = new Element("methodCall");
28    root ~= new Element("methodName", call.name);
29    root ~= encodeParams(call.params);
30    return toString(root);
31 }
32 
33 string encodeResponse(MethodResponseData resp) {
34    auto root = new Element("methodResponse");
35    if (resp.fault) {
36       enforce(resp.params.length == 1, new EncoderException("Fault response must contain exactly one parameter"));
37       auto fault = new Element("fault");
38       fault ~= encodeValue(resp.params[0]);
39       root ~= fault;
40    } else
41       root ~= encodeParams(resp.params);
42    return toString(root);
43 }
44 
45 private:
46 
47 Element encodeParams(Variant[] params) {
48    auto node = new Element("params");
49    foreach (ref par; params)
50       node ~= encodeParam(par);
51    return node;
52 }
53 
54 Element encodeParam(Variant param) {
55    auto node = new Element("param");
56    node ~= encodeValue(param);
57    return node;
58 }
59 
60 Element encodeValue(Variant param) {
61    auto node = new Element("value");
62    // TODO: allow associative arrays with keys of arbitrary type?
63    if (param.convertsTo!XmlRpcStruct)
64       node ~= encodeStructValue(param);
65    else if (param.convertsTo!XmlRpcArray)
66       node ~= encodeArrayValue(param);
67    else
68       node ~= encodePrimitiveValue(param);
69    return node;
70 }
71 
72 Element encodeStructValue(Variant param) {
73    auto structNode = new Element("struct");
74    foreach (key, ref value; param.get!XmlRpcStruct()) {
75       auto member = new Element("member");
76       member ~= new Element("name", key);
77       member ~= encodeValue(value);
78       structNode ~= member;
79    }
80    return structNode;
81 }
82 
83 Element encodeArrayValue(Variant param) {
84    auto array = new Element("array");
85    auto data = new Element("data");
86    foreach (ref value; param.get!XmlRpcArray)
87       data ~= encodeValue(value);
88    array ~= data;
89    return array;
90 }
91 
92 Element encodePrimitiveValue(Variant param) {
93    // Almost everything converts to boolean, thus we need to check the exact type match here:
94    if (param.type() == typeid(bool))
95       return new Element("boolean", param.get!bool() ? "1" : "0");
96 
97    if (param.convertsTo!int() || param.convertsTo!uint())
98       return new Element("int", param.toString());
99 
100    // Extension (64-bit signed integer)
101    if (param.convertsTo!long() || param.convertsTo!ulong())
102       return new Element("i8", param.toString());
103 
104    if (param.convertsTo!real())
105       return new Element("double", param.toString());
106 
107    if (param.convertsTo!(const(string))() || param.convertsTo!(const(dstring))() || param.convertsTo!(const(wstring))()) {
108       return new Element("string", param.toString());
109    }
110 
111    if (param.convertsTo!DateTime()) {
112       const dt = param.get!DateTime();
113       return new Element("dateTime.iso8601", dt.toISOString());
114    }
115 
116    if (param.convertsTo!(const(ubyte[]))()) {
117       const source = param.get!(const(ubyte[]))();
118       char[] encoded = Base64.encode(source);
119       return new Element("base64", to!string(encoded));
120    }
121 
122    // Extension (nil)
123    if (param.convertsTo!(typeof(null)) && param.get!(typeof(null))() == null)
124       return new Element("nil");
125 
126    throw new EncoderException(format("Unable to encode the value of type %s", param.type()));
127 }
128 
129 string toString(in Element e) {
130    return join(e.pretty(0), "\n");
131 }
132 
133 version (xmlrpc_unittest) unittest {
134    import xmlrpcc.decoder;
135 
136    static import xmlrpcc.paramconv;
137 
138    void assertResultsEqual(T)(T a, T b) {
139       writeln(a.toString());
140       //writeln(b.toString());
141       // Variant type does not compare arrays properly
142       assert(a.toString() == b.toString());
143    }
144 
145    // Hardcore parameter set
146    Variant[] params = [
147       Variant(123U), Variant(cast(const) "Our sun is dying."), Variant(0xc0a1_e5ce_d64b_17UL), Variant([Variant(123),
148       Variant(12.3), Variant("abc"d), Variant(0), Variant(true), Variant(null)
149    ]), Variant(DateTime(2013, 8, 25, 13, 38, 42)), Variant(cast(const(ubyte[])) x"de ad be ef"),
150       Variant(["null" : Variant(null)]), Variant(["true" : Variant(true)]), Variant(["false" : Variant(false)]),
151       Variant(["we" : Variant(["need" : Variant("to go deeper")])])];
152 
153    /*
154      * Call
155      */
156    auto methodCallData = MethodCallData("theMethod", params);
157    auto encoded = encodeCall(methodCallData);
158    MethodCallData decodedCall = decodeCall(encoded);
159    assertResultsEqual(decodedCall, methodCallData);
160 
161    /*
162      * Response
163      */
164    auto methodResponseData = MethodResponseData(false, params);
165    encoded = encodeResponse(methodResponseData);
166    MethodResponseData decodedResponse = decodeResponse(encoded);
167    assertResultsEqual(decodedResponse, methodResponseData);
168 
169    /*
170      * Fault response
171      */
172    auto faultParams = xmlrpcc.paramconv.paramsToVariantArray(["faultCode"w : Variant(42),
173       "faultString" : Variant("Fire in oxygen garden."d)]);
174    methodResponseData = MethodResponseData(true, faultParams);
175    encoded = encodeResponse(methodResponseData);
176    decodedResponse = decodeResponse(encoded);
177    assert(decodedResponse.fault);
178    assert(decodedResponse.params.length == 1);
179    assert(decodedResponse.params[0]["faultCode"] == 42);
180    assert(decodedResponse.params[0]["faultString"] == "Fire in oxygen garden.");
181 }