1 module text.xml.Encode;
2 
3 import boilerplate.util;
4 import dxml.util;
5 import dxml.writer;
6 import meta.attributesOrNothing;
7 import std.array;
8 import std.meta;
9 import std.traits;
10 import std.typecons;
11 import sumtype : match, SumType;
12 import text.xml.Convert;
13 import text.xml.Xml;
14 
15 /**
16  * The `text.xml.encode` function encodes an arbitrary type as XML.
17  * Each tagged field in the type is encoded.
18  * Tags are @(Xml.Attribute("attributeName")) and @(Xml.Element("tagName")).
19  * Types passed directly to `encode` must be annotated with an @(Xml.Element("...")) attribute.
20  * Child types must be annotated at their fields in the containing type.
21  * For array fields, their values are encoded sequentially.
22  * Nullable fields are omitted if they are null.
23  */
24 public string encode(T)(const T value)
25 in
26 {
27     static if (is(T == class))
28     {
29         assert(value !is null);
30     }
31 }
32 do
33 {
34     mixin enforceTypeHasElementTag!(T, "type passed to text.xml.encode");
35 
36     alias attributes = AliasSeq!(__traits(getAttributes, T));
37     auto writer = xmlWriter(appender!string);
38 
39     encodeNode!(T, attributes)(writer, value);
40 
41     return writer.output.data;
42 }
43 
44 private void encodeNode(T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value)
45 {
46     enum elementName = Xml.elementName!attributes(typeName!T).get;
47 
48     writer.openStartTag(elementName, Newline.no);
49 
50     // encode all the attribute members
51     static foreach (member; FilterMembers!(T, value, true))
52     {{
53         auto memberValue = __traits(getMember, value, member);
54         alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
55         alias PlainMemberT = typeof(cast() memberValue);
56         enum name = Xml.attributeName!memberAttrs(optionallyRemoveTrailingUnderline!member).get;
57 
58         static if (is(PlainMemberT : Nullable!Arg, Arg))
59         {
60             if (!memberValue.isNull)
61             {
62                 writer.writeAttr(name, encodeLeafImpl!(Arg, memberAttrs)(memberValue.get).encodeAttr, Newline.no);
63             }
64         }
65         else
66         {
67             writer.writeAttr(name, encodeLeafImpl!(PlainMemberT, memberAttrs)(memberValue).encodeAttr, Newline.no);
68         }
69     }}
70 
71     bool tagIsEmpty = true;
72 
73     static foreach (member; FilterMembers!(T, value, false))
74     {{
75         auto memberValue = __traits(getMember, value, member);
76         alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
77         alias PlainMemberT = typeof(cast() memberValue);
78         enum hasXmlTag = !Xml.elementName!memberAttrs(typeName!PlainMemberT).isNull
79             || udaIndex!(Xml.Text, memberAttrs) != -1;
80         enum isSumType = is(PlainMemberT : SumType!U, U...);
81 
82         static if (hasXmlTag || isSumType)
83         {
84             static if (is(PlainMemberT : Nullable!Arg, Arg))
85             {
86                 if (!memberValue.isNull)
87                 {
88                     tagIsEmpty = false;
89                 }
90             }
91             else
92             {
93                 tagIsEmpty = false;
94             }
95         }
96     }}
97 
98     writer.closeStartTag(tagIsEmpty ? EmptyTag.yes : EmptyTag.no);
99 
100     if (!tagIsEmpty)
101     {
102         static foreach (member; FilterMembers!(T, value, false))
103         {{
104             auto memberValue = __traits(getMember, value, member);
105             alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
106             alias PlainMemberT = typeof(cast() memberValue);
107             enum name = Xml.elementName!memberAttrs(typeName!PlainMemberT);
108 
109             static if (!name.isNull)
110             {
111                 enum string nameGet__ = name.get; // work around for weird compiler bug
112 
113                 encodeNodeImpl!(nameGet__, PlainMemberT, memberAttrs)(writer, memberValue);
114             }
115             else static if (udaIndex!(Xml.Text, memberAttrs) != -1)
116             {
117                 writer.writeText(memberValue.encodeText, Newline.no);
118             }
119             else static if (is(typeof(cast() memberValue) : SumType!U, U...))
120             {
121                 encodeSumType(writer, memberValue);
122             }
123         }}
124 
125         writer.writeEndTag(Newline.no);
126     }
127 }
128 
129 private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T value)
130 {
131     value.match!(staticMap!((const value) {
132         alias T = typeof(value);
133 
134         static if (is(T: U[], U))
135         {
136             alias BaseType = U;
137         }
138         else
139         {
140             alias BaseType = T;
141         }
142 
143         mixin enforceTypeHasElementTag!(BaseType, "every member type of SumType");
144 
145         alias attributes = AliasSeq!(__traits(getAttributes, BaseType));
146         enum name = Xml.elementName!attributes(typeName!BaseType).get;
147 
148         encodeNodeImpl!(name, T, attributes)(writer, value);
149     }, T.Types));
150 }
151 
152 private mixin template enforceTypeHasElementTag(T, string context)
153 {
154     static assert(
155         !Xml.elementName!(__traits(getAttributes, T))(typeName!T).isNull,
156         fullyQualifiedName!T ~
157         ": " ~ context ~ " must have an Xml.Element attribute indicating its element name.");
158 }
159 
160 private enum typeName(T) = typeof(cast() T.init).stringof;
161 
162 private template FilterMembers(T, alias value, bool keepXmlAttributes)
163 {
164     alias pred = ApplyLeft!(attrFilter, value, keepXmlAttributes);
165     alias FilterMembers = Filter!(pred, __traits(derivedMembers, T));
166 }
167 
168 private template attrFilter(alias value, bool keepXmlAttributes, string member)
169 {
170     // double-check that the member has a type to work around https://issues.dlang.org/show_bug.cgi?id=22214
171     static if (is(typeof(__traits(getMember, value, member)))
172         && __traits(compiles, { auto value = __traits(getMember, value, member); }))
173     {
174         alias attributes = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
175         static if (keepXmlAttributes)
176         {
177             enum bool attrFilter = !Xml.attributeName!(attributes)("").isNull;
178         }
179         else
180         {
181             enum bool attrFilter = Xml.attributeName!(attributes)("").isNull;
182         }
183     }
184     else
185     {
186         enum bool attrFilter = false;
187     }
188 }
189 
190 // test for https://issues.dlang.org/show_bug.cgi?id=22214
191 unittest
192 {
193     static struct S
194     {
195         struct T { }
196     }
197     S s;
198     static assert(attrFilter!(s, false, "T") == false);
199 }
200 
201 private void encodeNodeImpl(string name, T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value)
202 {
203     alias PlainT = typeof(cast() value);
204 
205     static if (__traits(compiles, __traits(getAttributes, T)))
206     {
207         alias typeAttributes = AliasSeq!(__traits(getAttributes, T));
208     }
209     else
210     {
211         alias typeAttributes = AliasSeq!();
212     }
213 
214     static if (is(PlainT : Nullable!Arg, Arg))
215     {
216         if (!value.isNull)
217         {
218             encodeNodeImpl!(name, Arg, attributes)(writer, value.get);
219         }
220     }
221     else static if (udaIndex!(Xml.Encode, attributes) != -1)
222     {
223         alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction;
224 
225         writer.openStartTag(name, Newline.no);
226         writer.closeStartTag;
227 
228         customEncoder(writer, value);
229         writer.writeEndTag(name, Newline.no);
230     }
231     else static if (udaIndex!(Xml.Encode, typeAttributes) != -1)
232     {
233         alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction;
234 
235         writer.openStartTag(name, Newline.no);
236         writer.closeStartTag;
237 
238         customEncoder(writer, value);
239         writer.writeEndTag(name, Newline.no);
240     }
241     else static if (isLeafType!(PlainT, attributes))
242     {
243         writer.openStartTag(name, Newline.no);
244         writer.closeStartTag;
245 
246         writer.writeText(encodeLeafImpl(value).encodeText, Newline.no);
247         writer.writeEndTag(name, Newline.no);
248     }
249     else static if (isIterable!PlainT)
250     {
251         alias IterationType(T) = typeof({ foreach (value; T.init) return value; assert(0); }());
252 
253         foreach (IterationType!PlainT a; value)
254         {
255             encodeNodeImpl!(name, typeof(a), attributes)(writer, a);
256         }
257     }
258     else
259     {
260         encodeNode!(PlainT, attributes)(writer, value);
261     }
262 }
263 
264 // must match encodeLeafImpl
265 private enum bool isLeafType(T, attributes...) =
266     udaIndex!(Xml.Encode, attributes) != -1
267     || udaIndex!(Xml.Encode, attributesOrNothing!T) != -1
268     || is(T == string)
269     || __traits(compiles, { Convert.toString(T.init); });
270 
271 private string encodeLeafImpl(T, attributes...)(T value)
272 {
273     alias typeAttributes = attributesOrNothing!T;
274 
275     static if (udaIndex!(Xml.Encode, attributes) != -1)
276     {
277         alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction;
278 
279         return customEncoder(value);
280     }
281     else static if (udaIndex!(Xml.Encode, typeAttributes) != -1)
282     {
283         alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction;
284 
285         return customEncoder(value);
286     }
287     else static if (is(T == string))
288     {
289         return value;
290     }
291     else static if (__traits(compiles, Convert.toString(value)))
292     {
293         return Convert.toString(value);
294     }
295     else
296     {
297         static assert(false, "Unknown value type: " ~ T.stringof);
298     }
299 }