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