1 module text.xml.Decode;
2 
3 import boilerplate.util : udaIndex;
4 static import dxml.util;
5 import meta.attributesOrNothing;
6 import meta.never;
7 import meta.SafeUnqual;
8 import std.format : format;
9 import sumtype;
10 import text.xml.Validation : enforceName, normalize, require, requireChild;
11 import text.xml.Tree;
12 public import text.xml.Xml;
13 
14 /**
15  * Throws: XmlException if the message is not well-formed or doesn't match the type
16  */
17 public T decode(T, alias customDecode = never)(string message)
18 {
19     import text.xml.Parser : parse;
20 
21     static assert(__traits(isSame, customDecode, never), "XML does not yet support a decode function");
22 
23     XmlNode rootNode = parse(message);
24 
25     return decodeXml!T(rootNode);
26 }
27 
28 /**
29  * Throws: XmlException if the XML element doesn't match the type
30  */
31 public T decodeXml(T)(XmlNode node)
32 {
33     import std.traits : fullyQualifiedName;
34 
35     static assert(
36         udaIndex!(Xml.Element, __traits(getAttributes, T)) != -1,
37         fullyQualifiedName!T ~
38         ": type passed to text.xml.decode must have an Xml.Element attribute indicating its element name.");
39 
40     enum name = __traits(getAttributes, T)[udaIndex!(Xml.Element, __traits(getAttributes, T))].name;
41 
42     node.enforceName(name);
43 
44     return decodeUnchecked!T(node);
45 }
46 
47 /**
48  * Throws: XmlException if the XML element doesn't match the type
49  * Returns: T, or the type returned from a decoder function defined on T.
50  */
51 public auto decodeUnchecked(T, attributes...)(XmlNode node)
52 {
53     import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, udaIndex;
54     import std.algorithm : map;
55     import std.meta : AliasSeq, anySatisfy, ApplyLeft;
56     import std.range : array, ElementType;
57     import std.string : strip;
58     import std.traits : fullyQualifiedName, isIterable, Unqual;
59     import std.typecons : Nullable, Tuple;
60 
61     static if (isNodeLeafType!(T, attributes))
62     {
63         return decodeNodeLeaf!(T, attributes)(node);
64     }
65     else
66     {
67         static assert(
68             __traits(hasMember, T, "ConstructorInfo"),
69             fullyQualifiedName!T ~ " does not have a boilerplate constructor!");
70 
71         auto builder = T.Builder();
72 
73         alias Info = Tuple!(string, "builderField", string, "constructorField");
74 
75         static foreach (string constructorField; T.ConstructorInfo.fields)
76         {{
77             enum builderField = optionallyRemoveTrailingUnderline!constructorField;
78 
79             mixin(formatNamed!q{
80                 alias Type = Unqual!(T.ConstructorInfo.FieldInfo.%(constructorField).Type);
81                 alias attributes = AliasSeq!(T.ConstructorInfo.FieldInfo.%(constructorField).attributes);
82 
83                 static if (is(Type : Nullable!Arg, Arg))
84                 {
85                     alias DecodeType = Arg;
86                     enum isNullable = true;
87                 }
88                 else
89                 {
90                     alias DecodeType = SafeUnqual!Type;
91                     enum isNullable = false;
92                 }
93 
94                 static if (is(Type : SumType!T, T...))
95                 {
96                     builder.%(builderField) = decodeSumType!T(node);
97                 }
98                 else static if (udaIndex!(Xml.Attribute, attributes) != -1)
99                 {
100                     enum name = attributes[udaIndex!(Xml.Attribute, attributes)].name;
101 
102                     static if (isNullable || T.ConstructorInfo.FieldInfo.%(constructorField).useDefault)
103                     {
104                         if (name in node.attributes)
105                         {
106                             builder.%(builderField) = decodeAttributeLeaf!(DecodeType, name, attributes)(node);
107                         }
108                     }
109                     else
110                     {
111                         builder.%(builderField) = decodeAttributeLeaf!(DecodeType, name, attributes)(node);
112                     }
113                 }
114                 else static if (udaIndex!(Xml.Element, attributes) != -1)
115                 {
116                     enum name = attributes[udaIndex!(Xml.Element, attributes)].name;
117 
118                     enum canDecodeNode = isNodeLeafType!(DecodeType, attributes)
119                         || __traits(compiles, .decodeUnchecked!(DecodeType, attributes)(XmlNode.init));
120 
121                     static if (canDecodeNode)
122                     {
123                         static if (isNullable || T.ConstructorInfo.FieldInfo.%(constructorField).useDefault)
124                         {
125                             static assert(
126                                 T.ConstructorInfo.FieldInfo.%(constructorField).useDefault,
127                                 format!"%s.%(constructorField) is Nullable, but missing @(This.Default)!"
128                                     (fullyQualifiedName!T));
129 
130                             auto child = node.findChild(name);
131 
132                             if (!child.isNull)
133                             {
134                                 builder.%(builderField) = decodeUnchecked!(DecodeType, attributes)(child.get);
135                             }
136                         }
137                         else
138                         {
139                             auto child = node.requireChild(name);
140 
141                             builder.%(builderField) = .decodeUnchecked!(DecodeType, attributes)(child);
142                         }
143                     }
144                     else static if (is(DecodeType: U[], U))
145                     {
146                         alias decodeChild = delegate U(XmlNode child)
147                         {
148                             return .decodeUnchecked!(U, attributes)(child);
149                         };
150 
151                         auto children = node.findChildren(name).map!decodeChild.array;
152 
153                         builder.%(builderField) = children;
154                     }
155                     else
156                     {
157                         pragma(msg, "While decoding field '" ~ name ~ "' of type " ~ DecodeType.stringof ~ ":");
158 
159                         // reproduce the error we swallowed earlier
160                         auto _ = .decodeUnchecked!(DecodeType, attributes)(XmlNode.init);
161                     }
162                 }
163                 else static if (udaIndex!(Xml.Text, attributes) != -1)
164                 {
165                     builder.%(builderField) = dxml.util.decodeXML(node.text);
166                 }
167                 else
168                 {
169                     enum sameField(string lhs, string rhs)
170                         = optionallyRemoveTrailingUnderline!lhs == optionallyRemoveTrailingUnderline!rhs;
171                     enum memberIsAliasedToThis = anySatisfy!(
172                         ApplyLeft!(sameField, constructorField),
173                         __traits(getAliasThis, T));
174 
175                     static if (memberIsAliasedToThis)
176                     {
177                         // decode inline
178                         builder.%(builderField) = .decodeUnchecked!(DecodeType, attributes)(node);
179                     }
180                     else
181                     {
182                         static assert(
183                             T.ConstructorInfo.FieldInfo.%(constructorField).useDefault,
184                             "Field " ~ fullyQualifiedName!T ~ ".%(constructorField) is required but has no Xml tag");
185                     }
186                 }
187             }.values(Info(builderField, constructorField)));
188         }}
189 
190         return builder.builderValue();
191     }
192 }
193 
194 /**
195  * Throws: XmlException if the XML element doesn't have a child matching exactly one of the subtypes,
196  * or if the child doesn't match the subtype.
197  */
198 private SumType!Types decodeSumType(Types...)(XmlNode node)
199 {
200     import std.algorithm : find, map, moveEmplace, sum;
201     import std.array : array, front;
202     import std.exception : enforce;
203     import std.meta : AliasSeq, staticMap;
204     import std.typecons : apply, Nullable, nullable;
205     import text.xml.XmlException : XmlException;
206 
207     Nullable!(SumType!Types)[Types.length] decodedValues;
208 
209     static foreach (i, Type; Types)
210     {{
211         static if (is(Type: U[], U))
212         {
213             alias attributes = AliasSeq!(__traits(getAttributes, U));
214             enum isArray = true;
215         }
216         else
217         {
218             alias attributes = AliasSeq!(__traits(getAttributes, Type));
219             enum isArray = false;
220         }
221 
222         static assert(
223             udaIndex!(Xml.Element, attributes) != -1,
224             fullyQualifiedName!Type ~
225             ": SumType component type must have an Xml.Element attribute indicating its element name.");
226 
227         enum name = attributes[udaIndex!(Xml.Element, attributes)].name;
228 
229         static if (isArray)
230         {
231             auto children = node.findChildren(name);
232 
233             if (!children.empty)
234             {
235                 decodedValues[i] = SumType!Types(children.map!(a => a.decodeUnchecked!U).array);
236             }
237         }
238         else
239         {
240             auto child = node.findChild(name);
241 
242             decodedValues[i] = child.apply!(a => SumType!Types(a.decodeUnchecked!Type));
243         }
244     }}
245 
246     const matchedValues = decodedValues[].map!(a => a.isNull ? 0 : 1).sum;
247 
248     enforce!XmlException(matchedValues != 0,
249         format!`Element "%s": no child element of %(%s, %)`(node.tag, [staticMap!(typeName, Types)]));
250     enforce!XmlException(matchedValues == 1,
251         format!`Element "%s": contained more than one of %(%s, %)`(node.tag, [staticMap!(typeName, Types)]));
252     return decodedValues[].find!(a => !a.isNull).front.get;
253 }
254 
255 private enum typeName(T) = T.stringof;
256 
257 private auto decodeAttributeLeaf(T, string name, attributes...)(XmlNode node)
258 {
259     alias typeAttributes = attributesOrNothing!T;
260 
261     static if (udaIndex!(Xml.Decode, attributes) != -1)
262     {
263         alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction;
264 
265         return decodeFunction(dxml.util.decodeXML(node.attributes[name]));
266     }
267     else static if (udaIndex!(Xml.Decode, typeAttributes) != -1)
268     {
269         alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction;
270 
271         return decodeFunction(dxml.util.decodeXML(node.attributes[name]));
272     }
273     else
274     {
275         return node.require!T(name);
276     }
277 }
278 
279 // must match decodeNodeLeaf
280 enum isNodeLeafType(T, attributes...) =
281     udaIndex!(Xml.Decode, attributes) != -1
282     || udaIndex!(Xml.Decode, attributesOrNothing!T) != -1
283     || __traits(compiles, XmlNode.init.require!(SafeUnqual!T)());
284 
285 private auto decodeNodeLeaf(T, attributes...)(XmlNode node)
286 {
287     alias typeAttributes = attributesOrNothing!T;
288 
289     static if (udaIndex!(Xml.Decode, attributes) != -1 || udaIndex!(Xml.Decode, typeAttributes) != -1)
290     {
291         static if (udaIndex!(Xml.Decode, attributes) != -1)
292         {
293             alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction;
294         }
295         else
296         {
297             alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction;
298         }
299 
300         static if (__traits(isTemplate, decodeFunction))
301         {
302             return decodeFunction!T(node);
303         }
304         else
305         {
306             return decodeFunction(node);
307         }
308     }
309     else static if (is(T == string))
310     {
311         return dxml.util.decodeXML(node.text).normalize;
312     }
313     else
314     {
315         return node.require!(SafeUnqual!T)();
316     }
317 }