1 module xmlrpcc.client;
2 
3 import std.datetime : Duration, dur;
4 import std.variant : Variant;
5 import std.string : format;
6 import std.stdio : writefln;
7 import std.conv : to;
8 
9 import xmlrpcc.encoder : encodeCall;
10 import xmlrpcc.decoder : decodeResponse;
11 import xmlrpcc.data : MethodCallData, MethodResponseData;
12 import xmlrpcc.paramconv : paramsToVariantArray, variantArrayToParams;
13 import xmlrpcc.error : XmlRpcException, MethodFaultException;
14 
15 
16 static import curl = std.net.curl;
17 
18 @trusted:
19 
20 class Client {
21    /**
22      * Params:
23      *     serverUri = Remote server endpoint, like "http://localhost:8000"
24      *     timeout   = HTTP(S) request timeout
25      */
26    nothrow this(string serverUri, Duration timeout = dur!"seconds"(10)) {
27       serverUri_ = serverUri;
28       timeout_ = timeout;
29    }
30 
31    /**
32      * Calls XML-RPC method. Parameters are converted automatically.
33      * Throws: TransportException on HTTP(S) error, MethodFaultException on the remote method fault
34      */
35    template call(string methodName, ReturnTypes...) {
36       final auto call(Args...)(Args args) {
37          auto requestParams = paramsToVariantArray(args);
38          auto callData = MethodCallData(methodName, requestParams);
39          Variant[] vars = rawCall(callData).params;
40 
41          // Perform automatic return type conversion if requested, otherwise return Variant[] as is
42          static if (ReturnTypes.length == 0)
43             return vars;
44          else
45             return variantArrayToParams!(ReturnTypes)(vars);
46       }
47    }
48 
49    /**
50      * Performs call to the XML-RPC method with no automatic type casting.
51      * Throws:
52      *     TransportException on HTTP(S) error
53      *     MethodFaultException on the remote method fault
54      */
55    final MethodResponseData rawCall(MethodCallData callData, bool suppressMethodFaultException = false) {
56       const requestString = encodeCall(callData);
57 
58       debug (xmlrpc)
59          writefln("client ==> %s", callData.toString());
60 
61       auto responseString = performHttpRequest(requestString);
62       auto responseData = decodeResponse(responseString);
63 
64       debug (xmlrpc)
65          writefln("client <== %s", responseData.toString());
66 
67       if (!suppressMethodFaultException && responseData.fault) {
68          Variant faultValue;
69          if (responseData.params.length > 0)
70             faultValue = responseData.params[0];
71 
72          const msg = format("XMLRPC method failure: %s / Call: %s", responseData.toString(), callData.toString());
73          throw new MethodFaultException(faultValue, msg);
74       }
75 
76       return responseData;
77    }
78 
79    @property nothrow string serverUri() const {
80       return serverUri_;
81    }
82 
83    @property nothrow Duration timeout() const {
84       return timeout_;
85    }
86 
87    @property nothrow void timeout(Duration timeout) {
88       timeout_ = timeout;
89    }
90 
91    @property nothrow void verifyPeer(bool val){
92        this.verifyPeer_ = val;
93    }
94 
95    @property nothrow bool verifyPeer(){
96        return verifyPeer_;
97    }
98 
99 
100 private:
101    string performHttpRequest(string data) {
102       try {
103          auto http = curl.HTTP(serverUri_);
104          http.operationTimeout = timeout_;
105          http.verifyPeer = verifyPeer;
106          return to!string(curl.post(serverUri_, data, http));
107       }
108       catch (curl.CurlException ex)
109          throw new TransportException(ex);
110    }
111 
112    const string serverUri_;
113    Duration timeout_;
114    bool verifyPeer_;
115 }
116 
117 class TransportException : XmlRpcException {
118    private this(Exception nested, string file = __FILE__, size_t line = __LINE__) {
119       this.nested = nested;
120       super(nested.msg, file, line);
121    }
122 
123    Exception nested;
124 }
125 
126 version (xmlrpc_client_unittest) unittest {
127    import std.stdio : writeln;
128    import xmlrpcc.data : prettyParams;
129    import std.exception : assertThrown;
130    import std.math : approxEqual;
131 
132    auto client = new Client("http://1.2.3.4", dur!"msecs"(10));
133 
134    // Should timeout:
135    assertThrown!TransportException(client.call!"boo"());
136 
137    client = new Client("http://phpxmlrpc.sourceforge.net/server.php");
138 
139    // Should fail and throw:
140    try {
141       Variant[] raw = client.call!"nonExistentMethod"("Wrong", "parameters");
142       assert(false);
143    }
144    catch (MethodFaultException ex) {
145       assert(ex.value["faultCode"] == 1);
146       assert(ex.value["faultString"].length);
147    }
148 
149    /*
150      * Misc logic checks
151      */
152    double resp1 = client.call!("examples.addtwodouble", double)(534.78, 168.36);
153    assert(approxEqual(resp1, 703.14));
154 
155    string resp2 = client.call!("examples.stringecho", string)("Hello Galaxy!");
156    assert(resp2 == "Hello Galaxy!");
157 
158    real resp2_1 = client.call!("examples.stringecho", real)("123.456"); // IMPLICIT CONVERSION
159    assert(approxEqual(resp2_1, 123.456));
160 
161    int[string] resp3 = client.call!("validator1.countTheEntities", int[string])("A < C ' > 45\" 12 &");
162    assert(1 == resp3["ctQuotes"]);
163    assert(1 == resp3["ctLeftAngleBrackets"]);
164    assert(1 == resp3["ctRightAngleBrackets"]);
165    assert(1 == resp3["ctAmpersands"]);
166    assert(1 == resp3["ctApostrophes"]);
167 
168    int[string][] arrayOfStructs = [["moe" : 1, "larry" : 2, "curly" : 3], ["moe" : -98, "larry" : 23, "curly" : -6]];
169    int resp4 = client.call!("validator1.arrayOfStructsTest", int)(arrayOfStructs);
170    assert(resp4 == -3);
171 }