1 module text.xml.DecodeTest;
2 
3 import boilerplate;
4 import dshould;
5 import std.datetime;
6 import sumtype : match, SumType;
7 import text.xml.Decode;
8 import text.xml.Tree;
9 import text.xml.Xml;
10 
11 @("XML elements are decoded to various types")
12 unittest
13 {
14     // given
15     const text = `
16         <root>
17             <IntValueElement>23</IntValueElement>
18             <StringValueElement>FOO</StringValueElement>
19             <BoolValueElement>true</BoolValueElement>
20             <NestedElement>
21                 <Element>BAR</Element>
22             </NestedElement>
23             <ArrayElement>1</ArrayElement>
24             <ArrayElement>2</ArrayElement>
25             <ArrayElement>3</ArrayElement>
26             <DateElement>2000-01-02</DateElement>
27             <SysTimeElement>2000-01-02T10:00:00Z</SysTimeElement>
28             <ContentElement attribute="hello">World</ContentElement>
29         </root>
30         `;
31 
32     // when
33     auto value = decode!Value(text);
34 
35     // then
36     auto expected = Value.Builder();
37 
38     with (expected)
39     {
40         intValue = 23;
41         stringValue = "FOO";
42         boolValue = true;
43         nestedValue = NestedValue("BAR");
44         arrayValue = [1, 2, 3];
45         dateValue = Date(2000, 1, 2);
46         sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z");
47         contentValue = ContentValue("hello", "World");
48     }
49 
50     value.should.equal(expected.value);
51 }
52 
53 @("XML attributes are decoded")
54 unittest
55 {
56     const expected = ValueWithAttribute(23);
57 
58     // given
59     const text = `<root intAttribute="23"/>`;
60 
61     // when
62     auto value = decode!ValueWithAttribute(text);
63 
64     // then
65     value.should.equal(expected);
66 }
67 
68 @("custom decoders are used on fields")
69 unittest
70 {
71     // given
72     const text = `<root asFoo="bla"><asBar>bla</asBar></root>`;
73 
74     // when
75     auto value = decode!ValueWithDecoders(text);
76 
77     // then
78     const expected = ValueWithDecoders("foo", "bar");
79 
80     value.should.equal(expected);
81 }
82 
83 @("custom decoders are used on types")
84 unittest
85 {
86     @(Xml.Element("root"))
87     struct Value
88     {
89         @(Xml.Element("foo"))
90         DecodeNodeTestType foo;
91 
92         @(Xml.Attribute("bar"))
93         DecodeAttributeTestType bar;
94 
95         mixin(GenerateAll);
96     }
97 
98     // given
99     const text = `<root bar="123"><foo>123</foo></root>`;
100 
101     // when
102     auto value = decode!Value(text);
103 
104     // then
105     const expected = Value(DecodeNodeTestType("foo"), DecodeAttributeTestType("bar"));
106 
107     value.should.equal(expected);
108 }
109 
110 @("field is Nullable")
111 unittest
112 {
113     import std.typecons : Nullable;
114 
115     @(Xml.Element("root"))
116     struct Value
117     {
118         @(Xml.Element("foo"))
119         @(This.Default)
120         Nullable!int foo;
121 
122         mixin(GenerateAll);
123     }
124 
125     // given
126     const text = `<root></root>`;
127 
128     // when
129     auto value = decode!Value(text);
130 
131     // Then
132     const expected = Value(Nullable!int());
133 
134     value.should.equal(expected);
135 }
136 
137 @("field and decoder are Nullable")
138 unittest
139 {
140     import std.typecons : Nullable;
141 
142     static Nullable!int returnsNull(const XmlNode)
143     {
144         return Nullable!int();
145     }
146 
147     @(Xml.Element("root"))
148     struct Value
149     {
150         @(Xml.Element("foo"))
151         @(Xml.Decode!returnsNull)
152         @(This.Default)
153         Nullable!int foo;
154 
155         mixin(GenerateAll);
156     }
157 
158     // given
159     const text = `<root><foo>5</foo></root>`;
160 
161     // when
162     auto value = decode!Value(text);
163 
164     // then
165     const expected = Value(Nullable!int());
166 
167     value.should.equal(expected);
168 }
169 
170 @("Element with whitespace around content")
171 unittest
172 {
173     @(Xml.Element("root"))
174     struct Value
175     {
176         @(Xml.Element("Content"))
177         string content;
178 
179         mixin(GenerateAll);
180     }
181 
182     // given
183     const text = `<root><Content>  Foo  Bar  </Content></root>`;
184 
185     // when
186     const value = decode!Value(text);
187 
188     // then
189     value.content.should.equal("Foo Bar");
190 }
191 
192 @("fields with characters requiring predefined entities")
193 unittest
194 {
195     @(Xml.Element("root"))
196     struct Value
197     {
198         @(Xml.Attribute("foo"))
199         string foo;
200 
201         @(Xml.Element("bar"))
202         string bar;
203 
204         mixin(GenerateThis);
205     }
206 
207     // given
208     const text = `<root foo="&lt;&amp;&quot;"><bar>&lt;&amp;]]&gt;</bar></root>`;
209 
210     // when
211     const value = decode!Value(text);
212 
213     // then
214     value.foo.should.equal(`<&"`);
215     value.bar.should.equal(`<&]]>`);
216 }
217 
218 @("fields with reserved names")
219 unittest
220 {
221     @(Xml.Element("root"))
222     struct Value
223     {
224         @(Xml.Attribute("version"))
225         string version_;
226 
227         mixin(GenerateThis);
228     }
229 
230     // given
231     const text = `<root version="1.0.0"/>`;
232 
233     // when
234     const value = decode!Value(text);
235 
236     // then
237     value.version_.should.equal(`1.0.0`);
238 }
239 
240 @("field aliased to this")
241 unittest
242 {
243     struct Nested
244     {
245         @(Xml.Attribute("foo"))
246         string foo;
247 
248         mixin(GenerateThis);
249     }
250 
251     @(Xml.Element("Value"))
252     struct Value
253     {
254         public Nested nested;
255 
256         alias nested this;
257 
258         mixin(GenerateThis);
259     }
260 
261     // given
262     const text = `<Value foo="bar"/>`;
263 
264     // when
265     const value = decode!Value(text);
266 
267     // then
268     value.foo.should.equal("bar");
269 }
270 
271 @("SumType")
272 unittest
273 {
274     import text.xml.XmlException : XmlException;
275     with (SumTypeFixture)
276     {
277         alias Either = SumType!(A, B);
278 
279         @(Xml.Element("Value"))
280         struct Value
281         {
282             /**
283             * One of A or B.
284             *
285             * equivalent to:
286             * ---
287             * @(Xml.Element("A"))
288             * @(This.Default)
289             * Nullable!A a;
290             *
291             * @(Xml.Element("B"))
292             * @(This.Default)
293             * Nullable!B b;
294             *
295             * invariant(!a.isNull && b.isNull || a.isNull && !b.isNull);
296             * ---
297             */
298             Either field;
299 
300             mixin(GenerateThis);
301         }
302 
303         // given/when/then
304         decode!Value(`<Value><A a="5"/></Value>`).should.equal(Value(Either(A(5))));
305 
306         decode!Value(`<Value><B b="3"/></Value>`).should.equal(Value(Either(B(3))));
307 
308         decode!Value(`<Value/>`).should.throwAn!XmlException
309             (`Element "Value": no child element of "A", "B"`);
310 
311         decode!Value(`<Value><A a="5"/><B b="3"/></Value>`).should.throwAn!XmlException
312             (`Element "Value": contained more than one of "A", "B"`);
313     }
314 }
315 
316 @("SumType with arrays")
317 unittest
318 {
319     import text.xml.XmlException : XmlException;
320 
321     with (SumTypeFixture)
322     {
323         alias Either = SumType!(A[], B[]);
324 
325         @(Xml.Element("Value"))
326         struct Value
327         {
328             /**
329             * either at least one A or at least one B
330             *
331             * equivalent to:
332             * ---
333             * @(Xml.Element("A"))
334             * @(This.Default)
335             * A[] as;
336             *
337             * @(Xml.Element("B"))
338             * @(This.Default)
339             * B[] bs;
340             *
341             * invariant(!a.empty && b.empty || a.empty && !b.empty);
342             * ---
343             */
344             Either field;
345 
346             mixin(GenerateThis);
347         }
348 
349         // given/when/then
350         decode!Value(`<Value><A a="5"/></Value>`).should.equal(Value(Either([A(5)])));
351 
352         decode!Value(`<Value><A a="5"/><A a="6"/></Value>`).should.equal(Value(Either([A(5), A(6)])));
353 
354         decode!Value(`<Value/>`).should.throwAn!XmlException
355             (`Element "Value": no child element of "A[]", "B[]"`);
356 
357         decode!Value(`<Value><A a="5"/><B b="3"/></Value>`).should.throwAn!XmlException
358             (`Element "Value": contained more than one of "A[]", "B[]"`);
359     }
360 }
361 
362 @("immutable arrays")
363 unittest
364 {
365     struct Value
366     {
367         @(Xml.Attribute("value"))
368         int value;
369 
370         mixin(GenerateThis);
371     }
372 
373     @(Xml.Element("Container"))
374     struct Container
375     {
376         @(Xml.Element("Value"))
377         immutable(Value)[] values;
378 
379         mixin(GenerateThis);
380     }
381 
382     // when
383     auto value = decode!Container(`<Container><Value value="1"/><Value value="2"/><Value value="3"/></Container>`);
384 
385     // then
386     auto expected = Container([Value(1), Value(2), Value(3)]);
387 
388     value.should.equal(expected);
389 }
390 
391 private struct SumTypeFixture
392 {
393     @(Xml.Element("A"))
394     static struct A
395     {
396         @(Xml.Attribute("a"))
397         int a;
398 
399         mixin(GenerateThis);
400     }
401 
402     @(Xml.Element("B"))
403     static struct B
404     {
405         @(Xml.Attribute("b"))
406         int b;
407 
408         mixin(GenerateThis);
409     }
410 }
411 
412 @(Xml.Element("root"))
413 private struct Value
414 {
415     @(Xml.Element("IntValueElement"))
416     public int intValue;
417 
418     @(Xml.Element("StringValueElement"))
419     public string stringValue;
420 
421     @(Xml.Element("BoolValueElement"))
422     public bool boolValue;
423 
424     @(Xml.Element("NestedElement"))
425     public NestedValue nestedValue;
426 
427     // Fails to compile when serializing const values
428     @(Xml.Element("ArrayElement"))
429     public int[] arrayValue;
430 
431     @(Xml.Element("DateElement"))
432     public Date dateValue;
433 
434     @(Xml.Element("SysTimeElement"))
435     public SysTime sysTimeValue;
436 
437     @(Xml.Element("ContentElement"))
438     public ContentValue contentValue;
439 
440     mixin (GenerateAll);
441 }
442 
443 private struct NestedValue
444 {
445     @(Xml.Element("Element"))
446     public string value;
447 
448     mixin (GenerateAll);
449 }
450 
451 private struct ContentValue
452 {
453     @(Xml.Attribute("attribute"))
454     public string attribute;
455 
456     @(Xml.Text)
457     public string content;
458 
459     mixin (GenerateAll);
460 }
461 
462 @(Xml.Element("root"))
463 private struct ValueWithAttribute
464 {
465     @(Xml.Attribute("intAttribute"))
466     public int value;
467 
468     mixin(GenerateThis);
469 }
470 
471 @(Xml.Element("root"))
472 private struct ValueWithDecoders
473 {
474     @(Xml.Attribute("asFoo"))
475     @(Xml.Decode!asFoo)
476     public string foo;
477 
478     @(Xml.Element("asBar"))
479     @(Xml.Decode!asBar)
480     public string bar;
481 
482     static string asFoo(string attribute)
483     {
484         attribute.should.equal("bla");
485 
486         return "foo";
487     }
488 
489     static string asBar(XmlNode node)
490     {
491         import std.string : strip;
492 
493         node.text.strip.should.equal("bla");
494 
495         return "bar";
496     }
497 
498     mixin(GenerateThis);
499 }
500 
501 @(Xml.Element)
502 @(Xml.Decode!decodeNodeTestType)
503 package struct DecodeNodeTestType
504 {
505     string value;
506 }
507 
508 package DecodeNodeTestType decodeNodeTestType(XmlNode node)
509 {
510     return DecodeNodeTestType("foo");
511 }
512 
513 @(Xml.Attribute)
514 @(Xml.Decode!decodeAttributeTestType)
515 package struct DecodeAttributeTestType
516 {
517     string value;
518 }
519 
520 package DecodeAttributeTestType decodeAttributeTestType(string)
521 {
522     return DecodeAttributeTestType("bar");
523 }