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