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 }