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