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="<&""><bar><&]]></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 }