1 module text.xml.DecodeTest;
2 
3 import boilerplate;
4 import dshould;
5 import std.datetime;
6 import std.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 of 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 @("array of SumTypes")
363 unittest
364 {
365     import text.xml.XmlException : XmlException;
366 
367     with (SumTypeFixture)
368     {
369         alias Either = SumType!(A, B);
370 
371         @(Xml.Element("Value"))
372         struct Value
373         {
374             // any number of either A or B
375             Either[] entries;
376 
377             mixin(GenerateThis);
378         }
379 
380         // given/when/then
381         decode!Value(`<Value><A a="5"/></Value>`).should.equal(Value([Either(A(5))]));
382 
383         decode!Value(`<Value><B b="5"/><A a="6"/></Value>`).should.equal(Value([Either(B(5)), Either(A(6))]));
384 
385         decode!Value(`<Value/>`).should.equal(Value([]));
386     }
387 }
388 
389 private struct SumTypeFixture
390 {
391     @(Xml.Element("A"))
392     static struct A
393     {
394         @(Xml.Attribute("a"))
395         int a;
396 
397         mixin(GenerateThis);
398     }
399 
400     @(Xml.Element("B"))
401     static struct B
402     {
403         @(Xml.Attribute("b"))
404         int b;
405 
406         mixin(GenerateThis);
407     }
408 }
409 
410 @("immutable arrays")
411 unittest
412 {
413     struct Value
414     {
415         @(Xml.Attribute("value"))
416         int value;
417 
418         mixin(GenerateThis);
419     }
420 
421     @(Xml.Element("Container"))
422     struct Container
423     {
424         @(Xml.Element("Value"))
425         immutable(Value)[] values;
426 
427         mixin(GenerateThis);
428     }
429 
430     // when
431     auto value = decode!Container(`<Container><Value value="1"/><Value value="2"/><Value value="3"/></Container>`);
432 
433     // then
434     auto expected = Container([Value(1), Value(2), Value(3)]);
435 
436     value.should.equal(expected);
437 }
438 
439 @("attribute/element without specified name")
440 unittest
441 {
442     struct Value
443     {
444         @(Xml.Attribute)
445         private int value_;
446 
447         mixin(GenerateThis);
448     }
449 
450     @(Xml.Element)
451     struct Container
452     {
453         @(Xml.Element)
454         immutable(Value)[] values;
455 
456         mixin(GenerateThis);
457     }
458 
459     // when
460     auto value = decode!Container(`<Container><Value value="1"/><Value value="2"/><Value value="3"/></Container>`);
461 
462     // then
463     auto expected = Container([Value(1), Value(2), Value(3)]);
464 
465     value.should.equal(expected);
466 }
467 
468 @(Xml.Element("root"))
469 private struct Value
470 {
471     @(Xml.Element("IntValueElement"))
472     public int intValue;
473 
474     @(Xml.Element("StringValueElement"))
475     public string stringValue;
476 
477     @(Xml.Element("BoolValueElement"))
478     public bool boolValue;
479 
480     @(Xml.Element("NestedElement"))
481     public NestedValue nestedValue;
482 
483     // Fails to compile when serializing const values
484     @(Xml.Element("ArrayElement"))
485     public int[] arrayValue;
486 
487     @(Xml.Element("DateElement"))
488     public Date dateValue;
489 
490     @(Xml.Element("SysTimeElement"))
491     public SysTime sysTimeValue;
492 
493     @(Xml.Element("ContentElement"))
494     public ContentValue contentValue;
495 
496     mixin (GenerateAll);
497 }
498 
499 private struct NestedValue
500 {
501     @(Xml.Element("Element"))
502     public string value;
503 
504     mixin (GenerateAll);
505 }
506 
507 private struct ContentValue
508 {
509     @(Xml.Attribute("attribute"))
510     public string attribute;
511 
512     @(Xml.Text)
513     public string content;
514 
515     mixin (GenerateAll);
516 }
517 
518 @(Xml.Element("root"))
519 private struct ValueWithAttribute
520 {
521     @(Xml.Attribute("intAttribute"))
522     public int value;
523 
524     mixin(GenerateThis);
525 }
526 
527 @(Xml.Element("root"))
528 private struct ValueWithDecoders
529 {
530     @(Xml.Attribute("asFoo"))
531     @(Xml.Decode!asFoo)
532     public string foo;
533 
534     @(Xml.Element("asBar"))
535     @(Xml.Decode!asBar)
536     public string bar;
537 
538     static string asFoo(string attribute)
539     {
540         attribute.should.equal("bla");
541 
542         return "foo";
543     }
544 
545     static string asBar(XmlNode node)
546     {
547         import std.string : strip;
548 
549         node.text.strip.should.equal("bla");
550 
551         return "bar";
552     }
553 
554     mixin(GenerateThis);
555 }
556 
557 @(Xml.Element)
558 @(Xml.Decode!decodeNodeTestType)
559 package struct DecodeNodeTestType
560 {
561     string value;
562 }
563 
564 package DecodeNodeTestType decodeNodeTestType(XmlNode node)
565 {
566     return DecodeNodeTestType("foo");
567 }
568 
569 @(Xml.Attribute)
570 @(Xml.Decode!decodeAttributeTestType)
571 package struct DecodeAttributeTestType
572 {
573     string value;
574 }
575 
576 package DecodeAttributeTestType decodeAttributeTestType(string)
577 {
578     return DecodeAttributeTestType("bar");
579 }