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     writer.openStartTag(attributes[udaIndex!(Xml.Element, attributes)].name, Newline.no);
47 
48     // encode all the attribute members
49     static foreach (member; FilterMembers!(T, value, true))
50     {{
51         auto memberValue = __traits(getMember, value, member);
52         alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
53         alias PlainMemberT = typeof(cast() memberValue);
54         enum name = memberAttrs[udaIndex!(Xml.Attribute, memberAttrs)].name;
55 
56         static if (is(PlainMemberT : Nullable!Arg, Arg))
57         {
58             if (!memberValue.isNull)
59             {
60                 writer.writeAttr(name, encodeLeafImpl!(Arg, memberAttrs)(memberValue.get).encodeAttr, Newline.no);
61             }
62         }
63         else
64         {
65             writer.writeAttr(name, encodeLeafImpl!(PlainMemberT, memberAttrs)(memberValue).encodeAttr, Newline.no);
66         }
67     }}
68 
69     bool tagIsEmpty = true;
70 
71     static foreach (member; FilterMembers!(T, value, false))
72     {{
73         auto memberValue = __traits(getMember, value, member);
74         alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
75         alias PlainMemberT = typeof(cast() memberValue);
76         enum hasXmlTag = udaIndex!(Xml.Element, memberAttrs) != -1 || udaIndex!(Xml.Text, memberAttrs) != -1;
77         enum isSumType = is(PlainMemberT : SumType!U, U...);
78 
79         static if (hasXmlTag || isSumType)
80         {
81             static if (is(PlainMemberT : Nullable!Arg, Arg))
82             {
83                 if (!memberValue.isNull)
84                 {
85                     tagIsEmpty = false;
86                 }
87             }
88             else
89             {
90                 tagIsEmpty = false;
91             }
92         }
93     }}
94 
95     writer.closeStartTag(tagIsEmpty ? EmptyTag.yes : EmptyTag.no);
96 
97     if (!tagIsEmpty)
98     {
99         static foreach (member; FilterMembers!(T, value, false))
100         {{
101             auto memberValue = __traits(getMember, value, member);
102             alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
103 
104             static if (udaIndex!(Xml.Element, memberAttrs) != -1)
105             {
106                 alias PlainMemberT = typeof(cast() memberValue);
107                 enum name = memberAttrs[udaIndex!(Xml.Element, memberAttrs)].name;
108 
109                 encodeNodeImpl!(name, PlainMemberT, memberAttrs)(writer, memberValue);
110             }
111             else static if (udaIndex!(Xml.Text, memberAttrs) != -1)
112             {
113                 writer.writeText(memberValue.encodeText, Newline.no);
114             }
115             else static if (is(typeof(cast() memberValue) : SumType!U, U...))
116             {
117                 encodeSumType(writer, memberValue);
118             }
119         }}
120 
121         writer.writeEndTag(Newline.no);
122     }
123 }
124 
125 private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T value)
126 {
127     value.match!(staticMap!((const value) {
128         alias T = typeof(value);
129 
130         static if (is(T: U[], U))
131         {
132             alias BaseType = U;
133         }
134         else
135         {
136             alias BaseType = T;
137         }
138 
139         mixin enforceTypeHasElementTag!(BaseType, "every member type of SumType");
140 
141         alias attributes = AliasSeq!(__traits(getAttributes, BaseType));
142         enum name = attributes[udaIndex!(Xml.Element, attributes)].name;
143 
144         encodeNodeImpl!(name, T, attributes)(writer, value);
145     }, T.Types));
146 }
147 
148 private mixin template enforceTypeHasElementTag(T, string context)
149 {
150     static assert(
151         udaIndex!(Xml.Element, __traits(getAttributes, T)) != -1,
152         fullyQualifiedName!T ~
153         ": " ~ context ~ " must have an Xml.Element attribute indicating its element name.");
154 }
155 
156 private template FilterMembers(T, alias value, bool keepXmlAttributes)
157 {
158     alias pred = ApplyLeft!(attrFilter, value, keepXmlAttributes);
159     alias FilterMembers = Filter!(pred, __traits(derivedMembers, T));
160 }
161 
162 private template attrFilter(alias value, bool keepXmlAttributes, string member)
163 {
164     static if (__traits(compiles, { return __traits(getMember, value, member); }))
165     {
166         alias attributes = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member)));
167         static if (keepXmlAttributes)
168         {
169             enum bool attrFilter = udaIndex!(Xml.Attribute, attributes) != -1;
170         }
171         else
172         {
173             enum bool attrFilter = udaIndex!(Xml.Attribute, attributes) == -1;
174         }
175     }
176     else
177     {
178         enum bool attrFilter = false;
179     }
180 }
181 
182 private void encodeNodeImpl(string name, T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value)
183 {
184     alias PlainT = typeof(cast() value);
185 
186     static if (__traits(compiles, __traits(getAttributes, T)))
187     {
188         alias typeAttributes = AliasSeq!(__traits(getAttributes, T));
189     }
190     else
191     {
192         alias typeAttributes = AliasSeq!();
193     }
194 
195     static if (is(PlainT : Nullable!Arg, Arg))
196     {
197         if (!value.isNull)
198         {
199             encodeNodeImpl!(name, Arg, attributes)(writer, value.get);
200         }
201     }
202     else static if (udaIndex!(Xml.Encode, attributes) != -1)
203     {
204         alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction;
205 
206         writer.openStartTag(name, Newline.no);
207         writer.closeStartTag;
208 
209         customEncoder(writer, value);
210         writer.writeEndTag(name, Newline.no);
211     }
212     else static if (udaIndex!(Xml.Encode, typeAttributes) != -1)
213     {
214         alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction;
215 
216         writer.openStartTag(name, Newline.no);
217         writer.closeStartTag;
218 
219         customEncoder(writer, value);
220         writer.writeEndTag(name, Newline.no);
221     }
222     else static if (isLeafType!(PlainT, attributes))
223     {
224         writer.openStartTag(name, Newline.no);
225         writer.closeStartTag;
226 
227         writer.writeText(encodeLeafImpl(value).encodeText, Newline.no);
228         writer.writeEndTag(name, Newline.no);
229     }
230     else static if (isIterable!PlainT)
231     {
232         alias IterationType(T) = typeof({ foreach (value; T.init) return value; assert(0); }());
233 
234         foreach (IterationType!PlainT a; value)
235         {
236             encodeNodeImpl!(name, typeof(a), attributes)(writer, a);
237         }
238     }
239     else
240     {
241         encodeNode!(PlainT, attributes)(writer, value);
242     }
243 }
244 
245 // must match encodeLeafImpl
246 private enum bool isLeafType(T, attributes...) =
247     udaIndex!(Xml.Encode, attributes) != -1
248     || udaIndex!(Xml.Encode, attributesOrNothing!T) != -1
249     || is(T == string)
250     || __traits(compiles, { Convert.toString(T.init); });
251 
252 private string encodeLeafImpl(T, attributes...)(T value)
253 {
254     alias typeAttributes = attributesOrNothing!T;
255 
256     static if (udaIndex!(Xml.Encode, attributes) != -1)
257     {
258         alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction;
259 
260         return customEncoder(value);
261     }
262     else static if (udaIndex!(Xml.Encode, typeAttributes) != -1)
263     {
264         alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction;
265 
266         return customEncoder(value);
267     }
268     else static if (is(T == string))
269     {
270         return value;
271     }
272     else static if (__traits(compiles, Convert.toString(value)))
273     {
274         return Convert.toString(value);
275     }
276     else
277     {
278         static assert(false, "Unknown value type: " ~ T.stringof);
279     }
280 }