1 /**
2  * Simple XML-RPC serializer.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.utils.xmlrpc;
15 
16 import std.string;
17 import std.conv;
18 import std.exception;
19 
20 import ae.utils.xml;
21 
22 /// Serialize a D value `v` to an XML node according to XML-RPC.
23 XmlNode valueToXml(T)(T v)
24 {
25 	static if (is(T : string))
26 		return (new XmlNode(XmlNodeType.Node, "value"))
27 			.addChild((new XmlNode(XmlNodeType.Node, "string"))
28 				.addChild(new XmlNode(XmlNodeType.Text, v))
29 			);
30 	else
31 	static if (is(T : long))
32 		return (new XmlNode(XmlNodeType.Node, "value"))
33 			.addChild((new XmlNode(XmlNodeType.Node, "integer"))
34 				.addChild(new XmlNode(XmlNodeType.Text, to!string(v)))
35 			);
36 	else
37 	static if (is(T U : U[]))
38 	{
39 		XmlNode data = new XmlNode(XmlNodeType.Node, "data");
40 		foreach (item; v)
41 			data.addChild(valueToXml(item));
42 		return (new XmlNode(XmlNodeType.Node, "value"))
43 			.addChild((new XmlNode(XmlNodeType.Node, "array"))
44 				.addChild(data)
45 			);
46 	}
47 	else
48 	static if (is(T==struct))
49 	{
50 		XmlNode s = new XmlNode(XmlNodeType.Node, "struct");
51 		foreach (i, field; v.tupleof)
52 			s.addChild((new XmlNode(XmlNodeType.Node, "member"))
53 				.addChild((new XmlNode(XmlNodeType.Node, "name"))
54 					.addChild(new XmlNode(XmlNodeType.Text, v.tupleof[i].stringof[2..$]))
55 				)
56 				.addChild(valueToXml(field))
57 			);
58 		return (new XmlNode(XmlNodeType.Node, "value"))
59 			.addChild(s);
60 	}
61 	else
62 	static if (is(typeof(T.keys)) && is(typeof(T.values)))
63 	{
64 		XmlNode s = new XmlNode(XmlNodeType.Node, "struct");
65 		foreach (key, value; v)
66 			s.addChild((new XmlNode(XmlNodeType.Node, "member"))
67 				.addChild((new XmlNode(XmlNodeType.Node, "name"))
68 					.addChild(new XmlNode(XmlNodeType.Text, key))
69 				)
70 				.addChild(valueToXml(value))
71 			);
72 		return (new XmlNode(XmlNodeType.Node, "value"))
73 			.addChild(s);
74 	}
75 	else
76 	static if (is(typeof(*v)))
77 		return valueToXml(*v);
78 	else
79 		static assert(0, "Can't encode " ~ T.stringof ~ " to XML-RPC");
80 }
81 
82 /// Format an XML-RPC method call.
83 XmlDocument formatXmlRpcCall(T...)(string methodName, T params)
84 {
85 	auto paramsNode = new XmlNode(XmlNodeType.Node, "params");
86 
87 	foreach (param; params)
88 		paramsNode.addChild((new XmlNode(XmlNodeType.Node, "param"))
89 			.addChild(valueToXml(param))
90 		);
91 
92 	auto doc =
93 		(new XmlDocument())
94 		.addChild((new XmlNode(XmlNodeType.Meta, "xml"))
95 			.addAttribute("version", "1.0")
96 		)
97 		.addChild((new XmlNode(XmlNodeType.Node, "methodCall"))
98 			.addChild((new XmlNode(XmlNodeType.Node, "methodName"))
99 				.addChild(new XmlNode(XmlNodeType.Text, methodName))
100 			)
101 			.addChild(paramsNode)
102 		);
103 	return cast(XmlDocument)doc;
104 }
105 
106 /// Parse a D value from an XML node according to XML-RPC.
107 T parseXmlValue(T)(XmlNode value)
108 {
109 	enforce(value.type==XmlNodeType.Node && value.tag == "value", "Expected <value> node");
110 	enforce(value.children.length==1, "Expected one <value> child");
111 	XmlNode typeNode = value[0];
112 	enforce(typeNode.type==XmlNodeType.Node, "Expected <value> child to be XML node");
113 	string valueType = typeNode.tag;
114 
115 	static if (is(T : string))
116 	{
117 		enforce(valueType == "string", "Expected <string>");
118 		enforce(typeNode.children.length==1, "Expected one <string> child");
119 		XmlNode contentNode = typeNode[0];
120 		enforce(contentNode.type==XmlNodeType.Text, "Expected <string> child to be text node");
121 		return contentNode.tag;
122 	}
123 	else
124 	static if (is(T == bool))
125 	{
126 		enforce(valueType == "boolean", "Expected <boolean>");
127 		enforce(typeNode.children.length==1, "Expected one <boolean> child");
128 		XmlNode contentNode = typeNode[0];
129 		enforce(contentNode.type==XmlNodeType.Text, "Expected <boolean> child to be text node");
130 		enforce(contentNode.tag == "0" || contentNode.tag == "1", "Expected <boolean> child to be 0 or 1");
131 		return contentNode.tag == "1";
132 	}
133 	else
134 	static if (is(T : long))
135 	{
136 		enforce(valueType == "integer" || valueType == "int" || valueType == "i4", "Expected <integer> or <int> or <i4>");
137 		enforce(typeNode.children.length==1, "Expected one <integer> child");
138 		XmlNode contentNode = typeNode[0];
139 		enforce(contentNode.type==XmlNodeType.Text, "Expected <integer> child to be text node");
140 		string s = contentNode.tag;
141 		static if (is(T==byte))
142 			return to!byte(s);
143 		else
144 		static if (is(T==ubyte))
145 			return to!ubyte(s);
146 		else
147 		static if (is(T==short))
148 			return to!short(s);
149 		else
150 		static if (is(T==ushort))
151 			return to!ushort(s);
152 		else
153 		static if (is(T==int))
154 			return to!int(s);
155 		else
156 		static if (is(T==uint))
157 			return to!uint(s);
158 		else
159 		static if (is(T==long))
160 			return to!long(s);
161 		else
162 		static if (is(T==ulong))
163 			return to!ulong(s);
164 		else
165 			static assert(0, "Don't know how to parse numerical type " ~ T.stringof);
166 	}
167 	else
168 	static if (is(T == double))
169 	{
170 		enforce(valueType == "double", "Expected <double>");
171 		enforce(typeNode.children.length==1, "Expected one <double> child");
172 		XmlNode contentNode = typeNode[0];
173 		enforce(contentNode.type==XmlNodeType.Text, "Expected <double> child to be text node");
174 		string s = contentNode.tag;
175 		return to!double(s);
176 	}
177 	else
178 	static if (is(T U : U[]))
179 	{
180 		enforce(valueType == "array", "Expected <array>");
181 		enforce(typeNode.children.length==1, "Expected one <array> child");
182 		XmlNode dataNode = typeNode[0];
183 		enforce(dataNode.type==XmlNodeType.Node && dataNode.tag == "data", "Expected <data>");
184 		T result = new U[dataNode.children.length];
185 		foreach (i, child; dataNode.children)
186 			result[i] = parseXmlValue!(U)(child);
187 		return result;
188 	}
189 	else
190 	static if (is(T==struct))
191 	{
192 		enforce(valueType == "struct", "Expected <struct>");
193 		T v;
194 		foreach (memberNode; typeNode.children)
195 		{
196 			enforce(memberNode.type==XmlNodeType.Node && memberNode.tag == "member", "Expected <member>");
197 			enforce(memberNode.children.length == 2, "Expected 2 <member> children");
198 			auto nameNode = memberNode[0];
199 			enforce(nameNode.type==XmlNodeType.Node && nameNode.tag == "name", "Expected <name>");
200 			enforce(nameNode.children.length == 1, "Expected one <name> child");
201 			XmlNode contentNode = nameNode[0];
202 			enforce(contentNode.type==XmlNodeType.Text, "Expected <name> child to be text node");
203 			string memberName = contentNode.tag;
204 
205 			bool found;
206 			foreach (i, field; v.tupleof)
207 				if (v.tupleof[i].stringof[2..$] == memberName)
208 				{
209 					v.tupleof[i] = parseXmlValue!(typeof(v.tupleof[i]))(memberNode[1]);
210 					found = true;
211 					break;
212 				}
213 			enforce(found, "Unknown field " ~ memberName);
214 		}
215 		return v;
216 	}
217 	else
218 		static assert(0, "Can't decode " ~ T.stringof ~ " from XML-RPC");
219 }
220 
221 /// Exception class used for XML-RPC response errors.
222 class XmlRpcException : Exception
223 {
224 	int faultCode; ///
225 	string faultString; ///
226 
227 	this(int faultCode, string faultString)
228 	{
229 		this.faultCode = faultCode;
230 		this.faultString = faultString;
231 		super(format("XML-RPC error %d (%s)", faultCode, faultString));
232 	} ///
233 }
234 
235 /// Parse an XML-RPC response.
236 /// On success, return the response return value, deserialized as `T`.
237 /// On failure, throw `XmlRpcException`.
238 T parseXmlRpcResponse(T)(XmlDocument doc)
239 {
240 	auto methodResponse = doc["methodResponse"];
241 	auto fault = methodResponse.findChild("fault");
242 	if (fault)
243 	{
244 		struct Fault
245 		{
246 			int faultCode;
247 			string faultString;
248 		}
249 		auto details = parseXmlValue!(Fault)(fault["value"]);
250 		throw new XmlRpcException(details.faultCode, details.faultString);
251 	}
252 
253 	auto params = methodResponse.findChild("params");
254 	enforce(params.children.length==1, "Only one response parameter supported");
255 	return parseXmlValue!(T)(params["param"][0]);
256 }