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 std.sumtype;
10 import text.xml.Tree;
11 import text.xml.Validation : enforceName, normalize, require, requireChild;
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     enum name = Xml.elementName!(__traits(getAttributes, T))(typeName!T);
36 
37     static assert(
38         !name.isNull,
39         fullyQualifiedName!T ~
40         ": type passed to text.xml.decode must have an Xml.Element attribute indicating its element name.");
41 
42     node.enforceName(name.get);
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             alias Type = Unqual!(__traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).Type);
80             alias attributes = AliasSeq!(
81                 __traits(getMember, 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                 __traits(getMember, builder, builderField) = decodeSumType!T(node);
97             }
98             else static if (is(Type : SumType!T[], T...))
99             {
100                 __traits(getMember, builder, builderField) = decodeSumTypeArray!T(node);
101             }
102             else static if (!Xml.attributeName!attributes(builderField).isNull)
103             {
104                 enum name = Xml.attributeName!attributes(builderField).get;
105 
106                 static if (isNullable || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault)
107                 {
108                     if (name in node.attributes)
109                     {
110                         __traits(getMember, builder, builderField)
111                             = decodeAttributeLeaf!(DecodeType, name, attributes)(node);
112                     }
113                 }
114                 else
115                 {
116                     __traits(getMember, builder, builderField)
117                         = decodeAttributeLeaf!(DecodeType, name, attributes)(node);
118                 }
119             }
120             else static if (!Xml.elementName!attributes(typeName!Type).isNull)
121             {
122 
123                 enum canDecodeNode = isNodeLeafType!(DecodeType, attributes)
124                     || __traits(compiles, .decodeUnchecked!(DecodeType, attributes)(XmlNode.init));
125 
126                 static if (canDecodeNode)
127                 {
128                     enum name = Xml.elementName!attributes(typeName!Type).get;
129 
130                     static if (isNullable
131                         || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault)
132                     {
133                         static assert(
134                             __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault,
135                             format!"%s." ~ constructorField ~ " is Nullable, but missing @(This.Default)!"
136                                 (fullyQualifiedName!T));
137 
138                         auto child = node.findChild(name);
139 
140                         if (!child.isNull)
141                         {
142                             __traits(getMember, builder, builderField)
143                                 = decodeUnchecked!(DecodeType, attributes)(child.get);
144                         }
145                     }
146                     else
147                     {
148                         auto child = node.requireChild(name);
149 
150                         __traits(getMember, builder, builderField)
151                             = .decodeUnchecked!(DecodeType, attributes)(child);
152                     }
153                 }
154                 else static if (is(DecodeType: U[], U))
155                 {
156                     enum name = Xml.elementName!attributes(typeName!U).get;
157 
158                     alias decodeChild = delegate U(XmlNode child)
159                     {
160                         return .decodeUnchecked!(U, attributes)(child);
161                     };
162 
163                     auto children = node.findChildren(name).map!decodeChild.array;
164 
165                     __traits(getMember, builder, builderField) = children;
166                 }
167                 else
168                 {
169                     pragma(msg, "While decoding field '" ~ constructorField ~ "' of type " ~ DecodeType.stringof ~ ":");
170 
171                     // reproduce the error we swallowed earlier
172                     auto _ = .decodeUnchecked!(DecodeType, attributes)(XmlNode.init);
173                 }
174             }
175             else static if (udaIndex!(Xml.Text, attributes) != -1)
176             {
177                 __traits(getMember, builder, builderField) = dxml.util.decodeXML(node.text);
178             }
179             else
180             {
181                 enum sameField(string lhs, string rhs)
182                     = optionallyRemoveTrailingUnderline!lhs == optionallyRemoveTrailingUnderline!rhs;
183                 enum memberIsAliasedToThis = anySatisfy!(
184                     ApplyLeft!(sameField, constructorField),
185                     __traits(getAliasThis, T));
186 
187                 static if (memberIsAliasedToThis)
188                 {
189                     // decode inline
190                     __traits(getMember, builder, builderField) = .decodeUnchecked!(DecodeType, attributes)(node);
191                 }
192                 else
193                 {
194                     static assert(
195                         __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault,
196                         "Field " ~ fullyQualifiedName!T ~ "." ~ constructorField ~ " is required but has no Xml tag");
197                 }
198             }
199         }}
200 
201         return builder.builderValue();
202     }
203 }
204 
205 /**
206  * Throws: XmlException if the XML element doesn't have a child matching exactly one of the subtypes,
207  * or if the child doesn't match the subtype.
208  */
209 private SumType!Types decodeSumType(Types...)(XmlNode node)
210 {
211     import std.algorithm : find, map, moveEmplace, sum;
212     import std.array : array, front;
213     import std.exception : enforce;
214     import std.meta : AliasSeq, staticMap;
215     import std.traits : fullyQualifiedName;
216     import std.typecons : apply, Nullable, nullable;
217     import text.xml.XmlException : XmlException;
218 
219     Nullable!(SumType!Types)[Types.length] decodedValues;
220 
221     static foreach (i, Type; Types)
222     {{
223         static if (is(Type: U[], U))
224         {
225             enum isArray = true;
226             alias BaseType = U;
227         }
228         else
229         {
230             enum isArray = false;
231             alias BaseType = Type;
232         }
233 
234         alias attributes = AliasSeq!(__traits(getAttributes, BaseType));
235 
236         static assert(
237             !Xml.elementName!attributes(typeName!BaseType).isNull,
238             fullyQualifiedName!Type ~
239             ": SumType component type must have an Xml.Element attribute indicating its element name.");
240 
241         enum name = Xml.elementName!attributes(typeName!BaseType).get;
242 
243         static if (isArray)
244         {
245             auto children = node.findChildren(name);
246 
247             if (!children.empty)
248             {
249                 decodedValues[i] = SumType!Types(children.map!(a => a.decodeUnchecked!U).array);
250             }
251         }
252         else
253         {
254             auto child = node.findChild(name);
255 
256             decodedValues[i] = child.apply!(a => SumType!Types(a.decodeUnchecked!Type));
257         }
258     }}
259 
260     const matchedValues = decodedValues[].map!(a => a.isNull ? 0 : 1).sum;
261 
262     enforce!XmlException(matchedValues != 0,
263         format!`Element "%s": no child element of %(%s, %)`(node.tag, [staticMap!(typeName, Types)]));
264     enforce!XmlException(matchedValues == 1,
265         format!`Element "%s": contained more than one of %(%s, %)`(node.tag, [staticMap!(typeName, Types)]));
266     return decodedValues[].find!(a => !a.isNull).front.get;
267 }
268 
269 private SumType!Types[] decodeSumTypeArray(Types...)(XmlNode node)
270 {
271     import std.meta : AliasSeq;
272     import std.traits : fullyQualifiedName;
273 
274     SumType!Types[] result;
275 
276     foreach (child; node.children)
277     {
278         static foreach (Type; Types)
279         {{
280             alias attributes = AliasSeq!(__traits(getAttributes, Type));
281 
282             static assert(
283                 !Xml.elementName!attributes(typeName!Type).isNull,
284                 fullyQualifiedName!Type ~
285                 ": SumType component type must have an Xml.Element attribute indicating its element name.");
286 
287             enum name = Xml.elementName!attributes(typeName!Type).get;
288 
289             if (child.tag == name)
290             {
291                 result ~= SumType!Types(child.decodeUnchecked!Type);
292             }
293         }}
294     }
295     return result;
296 }
297 
298 private enum typeName(T) = typeof(cast() T.init).stringof;
299 
300 private auto decodeAttributeLeaf(T, string name, attributes...)(XmlNode node)
301 {
302     alias typeAttributes = attributesOrNothing!T;
303 
304     static if (udaIndex!(Xml.Decode, attributes) != -1)
305     {
306         alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction;
307 
308         return decodeFunction(dxml.util.decodeXML(node.attributes[name]));
309     }
310     else static if (udaIndex!(Xml.Decode, typeAttributes) != -1)
311     {
312         alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction;
313 
314         return decodeFunction(dxml.util.decodeXML(node.attributes[name]));
315     }
316     else
317     {
318         return node.require!T(name);
319     }
320 }
321 
322 // must match decodeNodeLeaf
323 enum isNodeLeafType(T, attributes...) =
324     udaIndex!(Xml.Decode, attributes) != -1
325     || udaIndex!(Xml.Decode, attributesOrNothing!T) != -1
326     || __traits(compiles, XmlNode.init.require!(SafeUnqual!T)());
327 
328 private auto decodeNodeLeaf(T, attributes...)(XmlNode node)
329 {
330     alias typeAttributes = attributesOrNothing!T;
331 
332     static if (udaIndex!(Xml.Decode, attributes) != -1 || udaIndex!(Xml.Decode, typeAttributes) != -1)
333     {
334         static if (udaIndex!(Xml.Decode, attributes) != -1)
335         {
336             alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction;
337         }
338         else
339         {
340             alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction;
341         }
342 
343         static if (__traits(isTemplate, decodeFunction))
344         {
345             return decodeFunction!T(node);
346         }
347         else
348         {
349             return decodeFunction(node);
350         }
351     }
352     else static if (is(T == string))
353     {
354         return dxml.util.decodeXML(node.text).normalize;
355     }
356     else
357     {
358         return node.require!(SafeUnqual!T)();
359     }
360 }