1 module text.xml.EncodeTest;
2 
3 import boilerplate;
4 import dshould;
5 import dxml.writer;
6 import std.array;
7 import std.datetime;
8 import std.typecons;
9 import sumtype : match, SumType;
10 import text.xml.Encode;
11 import text.xml.Tree;
12 import text.xml.Writer;
13 import text.xml.Xml;
14 
15 @("fields tagged as Element are encoded as XML elements")
16 unittest
17 {
18     const expected =
19         `<root>` ~
20             `<IntValueElement>23</IntValueElement>` ~
21             `<StringValueElement>FOO</StringValueElement>` ~
22             `<BoolValueElement>true</BoolValueElement>` ~
23             `<NestedElement>` ~
24                 `<Element>BAR</Element>` ~
25             `</NestedElement>` ~
26             `<ArrayElement>1</ArrayElement>` ~
27             `<ArrayElement>2</ArrayElement>` ~
28             `<ArrayElement>3</ArrayElement>` ~
29             `<DateElement>2000-01-02</DateElement>` ~
30             `<SysTimeElement>2000-01-02T10:00:00Z</SysTimeElement>` ~
31             `<ContentElement attribute="hello">World</ContentElement>` ~
32         `</root>`
33     ;
34 
35     // given
36     const value = (){
37         import text.time.Convert : Convert;
38 
39         with (Value.Builder())
40         {
41             intValue = 23;
42             stringValue = "FOO";
43             boolValue = true;
44             nestedValue = NestedValue("BAR");
45             arrayValue = [1, 2, 3];
46             dateValue = Date(2000, 1, 2);
47             sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z");
48             contentValue = ContentValue("hello", "World");
49             return value;
50         }
51     }();
52 
53     // when
54     auto text = encode(value);
55 
56     // then
57     text.should.equal(expected);
58 }
59 
60 @("fields tagged as Attribute are encoded as XML attributes")
61 unittest
62 {
63     const expected = `<root intAttribute="23"/>`;
64 
65     // given
66     const valueWithAttribute = ValueWithAttribute(23);
67 
68     // when
69     auto text = encode(valueWithAttribute);
70 
71     // then
72     text.should.equal(expected);
73 }
74 
75 @("custom encoders are used on fields")
76 unittest
77 {
78     // given
79     const value = ValueWithEncoders("bla", "bla");
80 
81     // when
82     auto text = encode(value);
83 
84     // then
85     const expected = `<root asFoo="foo"><asBar>bar</asBar></root>`;
86 
87     text.should.equal(expected);
88 }
89 
90 @("custom encoders are used on types")
91 unittest
92 {
93     @(Xml.Element("root"))
94     struct Value
95     {
96         @(Xml.Element("foo"))
97         EncodeNodeTestType foo;
98 
99         @(Xml.Attribute("bar"))
100         EncodeAttributeTestType bar;
101     }
102 
103     // given
104     const value = Value(EncodeNodeTestType(), EncodeAttributeTestType());
105 
106     // when
107     auto text = .encode(value);
108 
109     // then
110     const expected = `<root bar="123"><foo>123</foo></root>`;
111 
112     text.should.equal(expected);
113 }
114 
115 @("custom encoder on Nullable element")
116 unittest
117 {
118     @(Xml.Element("root"))
119     struct Value
120     {
121         @(Xml.Element("foo"))
122         @(Xml.Encode!encodeNodeTestType)
123         Nullable!EncodeNodeTestType foo;
124     }
125 
126     // given
127     const value = Value(Nullable!EncodeNodeTestType());
128 
129     // when
130     const text = .encode(value);
131 
132     // then
133     const expected = `<root/>`;
134 
135     text.should.equal(expected);
136 }
137 
138 @("fields with characters requiring predefined entities")
139 unittest
140 {
141     @(Xml.Element("root"))
142     struct Value
143     {
144         @(Xml.Attribute("foo"))
145         string foo;
146 
147         @(Xml.Element("bar"))
148         string bar;
149     }
150 
151     // given
152     enum invalidInAttr = `<&"`;
153     enum invalidInText = `<&]]>`;
154     const value = Value(invalidInAttr, invalidInText);
155 
156     // when
157     auto text = .encode(value);
158 
159     // then
160     const expected = `<root foo="&lt;&amp;&quot;"><bar>&lt;&amp;]]&gt;</bar></root>`;
161 
162     text.should.equal(expected);
163 }
164 
165 @("regression: encodes optional elements with arrays")
166 unittest
167 {
168     struct Nested
169     {
170         @(Xml.Element("item"))
171         string[] items;
172     }
173 
174     @(Xml.Element("root"))
175     struct Root
176     {
177         @(Xml.Element("foo"))
178         Nullable!Nested nested;
179     }
180 
181     // given
182     const root = Root(Nullable!Nested(Nested(["foo", "bar"])));
183 
184     // when
185     const text = root.encode;
186 
187     // then
188     text.should.equal(`<root><foo><item>foo</item><item>bar</item></foo></root>`);
189 }
190 
191 @(Xml.Element("root"))
192 private struct Value
193 {
194     @(Xml.Element("IntValueElement"))
195     public int intValue;
196 
197     @(Xml.Element("StringValueElement"))
198     public string stringValue;
199 
200     @(Xml.Element("BoolValueElement"))
201     public bool boolValue;
202 
203     @(Xml.Element("NestedElement"))
204     public NestedValue nestedValue;
205 
206     // Fails to compile when serializing const values
207     @(Xml.Element("ArrayElement"))
208     public int[] arrayValue;
209 
210     @(Xml.Element("DateElement"))
211     public Date dateValue;
212 
213     @(Xml.Element("SysTimeElement"))
214     public SysTime sysTimeValue;
215 
216     @(Xml.Element("ContentElement"))
217     public ContentValue contentValue;
218 
219     mixin (GenerateAll);
220 }
221 
222 private struct NestedValue
223 {
224     @(Xml.Element("Element"))
225     public string value;
226 
227     mixin (GenerateAll);
228 }
229 
230 private struct ContentValue
231 {
232     @(Xml.Attribute("attribute"))
233     public string attribute;
234 
235     @(Xml.Text)
236     public string content;
237 
238     mixin (GenerateAll);
239 }
240 
241 @(Xml.Element("root"))
242 private struct ValueWithAttribute
243 {
244     @(Xml.Attribute("intAttribute"))
245     public int value;
246 
247     mixin(GenerateAll);
248 }
249 
250 @(Xml.Element("root"))
251 private struct ValueWithEncoders
252 {
253     @(Xml.Attribute("asFoo"))
254     @(Xml.Encode!asFoo)
255     public string foo;
256 
257     @(Xml.Element("asBar"))
258     @(Xml.Encode!asBar)
259     public string bar;
260 
261     static string asFoo(string field)
262     {
263         field.should.equal("bla");
264 
265         return "foo";
266     }
267 
268     static void asBar(ref XMLWriter!(Appender!string) writer, string field)
269     {
270         field.should.equal("bla");
271 
272         writer.writeText("bar", Newline.no);
273     }
274 
275     mixin(GenerateThis);
276 }
277 
278 package void encodeNodeTestType(ref XMLWriter!(Appender!string) writer, EncodeNodeTestType)
279 {
280     writer.writeText("123", Newline.no);
281 }
282 
283 @(Xml.Encode!encodeNodeTestType)
284 package struct EncodeNodeTestType
285 {
286 }
287 
288 package string encodeAttributeTestType(EncodeAttributeTestType)
289 {
290     return "123";
291 }
292 
293 @(Xml.Encode!encodeAttributeTestType)
294 package struct EncodeAttributeTestType
295 {
296 }
297 
298 @("struct with optional date attribute")
299 unittest
300 {
301     @(Xml.Element("root"))
302     static struct NullableAttributes
303     {
304         @(Xml.Attribute("date"))
305         @(This.Default)
306         Nullable!Date date;
307 
308         mixin(GenerateThis);
309     }
310 
311     // given
312     const root = NullableAttributes();
313 
314     // when
315     const text = root.encode;
316 
317     // then
318     text.should.equal(`<root/>`);
319 }
320 
321 @("SumType")
322 unittest
323 {
324     with (SumTypeFixture)
325     {
326         alias Either = SumType!(A, B);
327 
328         @(Xml.Element("root"))
329         static struct Struct
330         {
331             Either field;
332 
333             mixin(GenerateThis);
334         }
335 
336         // given/when/then
337         Struct(Either(A(5))).encode.should.equal(`<root><A a="5"/></root>`);
338 
339         Struct(Either(B(3))).encode.should.equal(`<root><B b="3"/></root>`);
340     }
341 }
342 
343 @("SumType with arrays")
344 unittest
345 {
346     with (SumTypeFixture)
347     {
348         alias Either = SumType!(A[], B[]);
349 
350         @(Xml.Element("root"))
351         static struct Struct
352         {
353             Either field;
354 
355             mixin(GenerateThis);
356         }
357 
358         // given/when/then
359         Struct(Either([A(5), A(6)])).encode.should.equal(`<root><A a="5"/><A a="6"/></root>`);
360     }
361 }
362 
363 private struct SumTypeFixture
364 {
365     @(Xml.Element("A"))
366     static struct A
367     {
368         @(Xml.Attribute("a"))
369         int a;
370     }
371 
372     @(Xml.Element("B"))
373     static struct B
374     {
375         @(Xml.Attribute("b"))
376         int b;
377     }
378 }
379 
380 @("attribute/element without specified name")
381 unittest
382 {
383     struct Value
384     {
385         @(Xml.Attribute)
386         private int value_;
387 
388         mixin(GenerateThis);
389     }
390 
391     @(Xml.Element)
392     struct Container
393     {
394         @(Xml.Element)
395         immutable(Value)[] values;
396 
397         mixin(GenerateThis);
398     }
399 
400     // when
401     const text = Container([Value(1), Value(2), Value(3)]).encode;
402 
403     // then
404     text.should.equal(`<Container><Value value="1"/><Value value="2"/><Value value="3"/></Container>`);
405 }