1 module text.json.DecodeTest; 2 3 import boilerplate; 4 import dshould; 5 import std.datetime; 6 import std.json; 7 import text.json.Decode; 8 import text.json.Json; 9 import text.json.ParserMarker; 10 11 static foreach (fromJsonValue; [false, true]) 12 { 13 @("JSON text with various types is decoded" ~ (fromJsonValue ? " from JSONValue" : "")) 14 unittest 15 { 16 import std.typecons : Nullable, nullable; 17 18 // given 19 const text = ` 20 { 21 "IntValueElement": 23, 22 "StringValueElement": "FOO", 23 "BoolValueElement": true, 24 "NestedElement": { 25 "Element": "Bar" 26 }, 27 "ArrayElement": [1, 2, 3], 28 "AssocArrayElement": { 29 "foo": "bar", 30 "baz": "whee" 31 }, 32 "DateElement": "2000-01-02", 33 "SysTimeElement": "2000-01-02T10:00:00Z" 34 } 35 `; 36 37 38 // when 39 static if (fromJsonValue) 40 { 41 auto value = decodeJson!Value(text.parseJSON); 42 } 43 else 44 { 45 auto value = decode!Value(text); 46 } 47 48 // then 49 50 auto expected = Value.Builder(); 51 52 with (expected) 53 { 54 import text.time.Convert : Convert; 55 56 intValue = 23; 57 stringValue = "FOO"; 58 boolValue = true; 59 nestedValue = NestedValue("Bar"); 60 arrayValue = [1, 2, 3]; 61 assocArray = ["foo": "bar", "baz": "whee"]; 62 dateValue = Date(2000, 1, 2); 63 sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z"); 64 } 65 66 value.should.equal(expected.value); 67 } 68 } 69 70 @("Nullable fields are optional") 71 unittest 72 { 73 decode!OptionalValues(`{}`).should.not.throwAn!Exception; 74 } 75 76 @("informative errors are reported when failing to decode types") 77 unittest 78 { 79 decode!OptionalValues(`{ "boolValue": "" }`).should.throwA!JSONException 80 (`Invalid JSON: text.json.DecodeTest.OptionalValues.boolValue expected bool, but got ""`); 81 decode!OptionalValues(`{ "intValue": "" }`).should.throwA!JSONException 82 (`Invalid JSON: text.json.DecodeTest.OptionalValues.intValue expected int, but got ""`); 83 decode!OptionalValues(`{ "enumValue": "B" }`).should.throwA!JSONException 84 (`Invalid JSON: text.json.DecodeTest.OptionalValues.enumValue expected member of Enum, but got "B"`); 85 decode!OptionalValues(`{ "enumValue": 5 }`).should.throwA!JSONException 86 (`Invalid JSON: text.json.DecodeTest.OptionalValues.enumValue expected enum string, but got 5`); 87 decode!OptionalValues(`{ "stringValue": 5 }`).should.throwA!JSONException 88 (`Invalid JSON: text.json.DecodeTest.OptionalValues.stringValue expected string, but got 5`); 89 decode!OptionalValues(`{ "arrayValue": [""] }`).should.throwA!JSONException 90 (`Invalid JSON: text.json.DecodeTest.OptionalValues.arrayValue[0] expected int, but got ""`); 91 } 92 93 struct OptionalValues 94 { 95 import std.typecons : Nullable; 96 97 enum Enum 98 { 99 A 100 } 101 102 Nullable!bool boolValue; 103 Nullable!int intValue; 104 Nullable!Enum enumValue; 105 Nullable!string stringValue; 106 Nullable!(int[]) arrayValue; 107 108 mixin(GenerateThis); 109 } 110 111 @("custom decoders are used on fields") 112 unittest 113 { 114 // given 115 const text = `{ "asFoo": "foo", "asBar": "bar" }`; 116 117 // when 118 auto value = decode!ValueWithDecoders(text); 119 120 // then 121 const expected = ValueWithDecoders("foobla", "barbla"); 122 123 value.should.equal(expected); 124 } 125 126 @("custom decoders are used on a type") 127 unittest 128 { 129 // given 130 const text = `{ "field": "bla" }`; 131 132 // when 133 struct Value 134 { 135 TypeWithDecoder field; 136 137 mixin(GenerateThis); 138 } 139 140 auto value = decode!Value(text); 141 142 // then 143 const expected = Value(TypeWithDecoder("123")); 144 145 value.should.equal(expected); 146 } 147 148 @("custom decoder with int array") 149 unittest 150 { 151 // when 152 const value = decode!TypeWithIntArrayDecoder(`[2, 3, 4]`); 153 154 // then 155 auto arr = [2, 3, 4]; 156 auto exp = TypeWithIntArrayDecoder(arr); 157 158 value.should.equal(exp); 159 } 160 161 @(Json.Decode!decodeTypeWithIntArrayDecoder) 162 struct TypeWithIntArrayDecoder 163 { 164 int[] value; 165 } 166 167 TypeWithIntArrayDecoder decodeTypeWithIntArrayDecoder(JSONValue value) 168 { 169 return TypeWithIntArrayDecoder(decodeJson!(int[])(value)); 170 } 171 172 @("enums are decoded from strings") 173 unittest 174 { 175 enum Enum 176 { 177 A 178 } 179 180 struct Value 181 { 182 Enum field; 183 184 mixin(GenerateAll); 185 } 186 187 // given 188 const text = `{ "field": "A" }`; 189 190 // when 191 const value = decode!Value(text); 192 193 // then 194 const expected = Value(Enum.A); 195 196 value.should.equal(expected); 197 } 198 199 @("alias-this is decoded from inline keys") 200 unittest 201 { 202 struct A 203 { 204 int value2; 205 206 mixin(GenerateAll); 207 } 208 209 struct B 210 { 211 int value1; 212 213 A a; 214 215 alias a this; 216 217 mixin(GenerateAll); 218 } 219 220 // given 221 const text = `{ "value1": 3, "value2": 5 }`; 222 223 // when 224 const actual = decode!B(text); 225 226 // then 227 const expected = B(3, A(5)); 228 229 actual.should.equal(expected); 230 } 231 232 @("alias-this is decoded from inline keys for aliased methods") 233 unittest 234 { 235 struct A 236 { 237 int value2; 238 239 mixin(GenerateAll); 240 } 241 242 struct B 243 { 244 int value1; 245 246 @ConstRead 247 A a_; 248 249 mixin(GenerateAll); 250 251 alias a this; 252 } 253 254 // given 255 const text = `{ "value1": 3, "value2": 5 }`; 256 257 // when 258 const actual = decode!B(text); 259 260 // then 261 const expected = B(3, A(5)); 262 263 actual.should.equal(expected); 264 } 265 266 static foreach (bool useJsonValueRange; [false, true]) 267 { 268 @("array of structs with alias-this is decoded" ~ (useJsonValueRange ? " from JsonStream" : "")) 269 unittest 270 { 271 struct A 272 { 273 int a; 274 275 mixin(GenerateAll); 276 } 277 278 struct B 279 { 280 A a; 281 282 int b; 283 284 alias a this; 285 286 mixin(GenerateAll); 287 } 288 289 // given 290 const text = `[{ "a": 1, "b": 2 }, { "a": 3, "b": 4}]`; 291 292 // when 293 static if (useJsonValueRange) 294 { 295 const actual = decodeJson!(B[])(text.parseJSON); 296 } 297 else 298 { 299 const actual = decode!(B[])(text); 300 } 301 302 // then 303 const expected = [B(A(1), 2), B(A(3), 4)]; 304 305 actual.should.equal(expected); 306 } 307 } 308 309 struct NestedValue 310 { 311 @(Json("Element")) 312 public string value; 313 314 mixin (GenerateAll); 315 } 316 317 struct Value 318 { 319 @(Json("IntValueElement")) 320 public int intValue; 321 322 @(Json("StringValueElement")) 323 public string stringValue; 324 325 @(Json("BoolValueElement")) 326 public bool boolValue; 327 328 @(Json("NestedElement")) 329 public NestedValue nestedValue; 330 331 @(Json("ArrayElement")) 332 public const int[] arrayValue; 333 334 @(Json("AssocArrayElement")) 335 public string[string] assocArray; 336 337 @(Json("DateElement")) 338 public Date dateValue; 339 340 @(Json("SysTimeElement")) 341 public SysTime sysTimeValue; 342 343 mixin (GenerateAll); 344 } 345 346 struct ValueWithDecoders 347 { 348 @(Json("asFoo")) 349 @(Json.Decode!fromFoo) 350 public string foo; 351 352 @(Json("asBar")) 353 @(Json.Decode!fromBar) 354 public string bar; 355 356 static string fromFoo(JSONValue value) 357 { 358 value.str.should.equal("foo"); 359 360 return "foobla"; 361 } 362 363 static string fromBar(JSONValue value) 364 { 365 value.str.should.equal("bar"); 366 367 return "barbla"; 368 } 369 370 mixin(GenerateThis); 371 } 372 373 @(Json.Decode!decodeTypeWithDecoder) 374 struct TypeWithDecoder 375 { 376 string value; 377 } 378 379 TypeWithDecoder decodeTypeWithDecoder(JSONValue value) 380 { 381 value.should.equal(JSONValue("bla")); 382 383 return TypeWithDecoder("123"); 384 } 385 386 @("transform functions may modify the values that are decoded") 387 unittest 388 { 389 import std.conv : to; 390 391 struct InnerDto 392 { 393 string encodedValue; 394 395 mixin(GenerateThis); 396 } 397 398 struct Inner 399 { 400 int value; 401 402 mixin(GenerateThis); 403 } 404 405 struct Struct 406 { 407 Inner inner; 408 409 mixin(GenerateThis); 410 } 411 412 alias transform(T : Inner) = (InnerDto innerDto) => 413 Inner(innerDto.encodedValue.to!int); 414 415 // !!! important to instantiate transform somewhere, to shake out errors 416 assert(transform!Inner(InnerDto("3")) == Inner(3)); 417 418 // given 419 const text = `{ "inner": { "encodedValue": "5" } }`; 420 421 // when 422 const actual = decode!(Struct, transform)(text); 423 424 // then 425 const expected = Struct(Inner(5)); 426 427 actual.should.equal(expected); 428 } 429 430 @("transform function with JSONValue parameter") 431 unittest 432 { 433 import std.conv : to; 434 435 struct Inner 436 { 437 int value; 438 439 mixin(GenerateThis); 440 } 441 442 struct Struct 443 { 444 Inner inner; 445 446 mixin(GenerateThis); 447 } 448 449 alias transform(T : Inner) = (JSONValue json) => 450 Inner(json.str.to!int); 451 452 // !!! important to instantiate transform somewhere, to shake out errors 453 assert(transform!Inner(JSONValue("3")) == Inner(3)); 454 455 // given 456 const text = `{ "inner": "5" }`; 457 458 // when 459 const actual = decode!(Struct, transform)(text); 460 461 // then 462 const expected = Struct(Inner(5)); 463 464 actual.should.equal(expected); 465 } 466 467 @("decode const array") 468 unittest 469 { 470 // given 471 const text = `[1, 2, 3]`; 472 473 // when 474 const actual = decode!(const(int[]))(text); 475 476 // Then 477 const expected = [1, 2, 3]; 478 479 actual.should.equal(expected); 480 } 481 482 @("missing fields") 483 unittest 484 { 485 // given 486 const text = `{}`; 487 488 struct S 489 { 490 int field; 491 492 mixin(GenerateThis); 493 } 494 495 // when/then 496 decode!S(text).should.throwA!JSONException("expected S.field, but got {}"); 497 } 498 499 @("decode object from non-object") 500 unittest 501 { 502 // given 503 const text = `[]`; 504 505 struct S 506 { 507 int field; 508 509 mixin(GenerateThis); 510 } 511 512 // when/then 513 decode!S(text).should.throwA!JSONException; 514 } 515 516 @("struct with version_ field") 517 unittest 518 { 519 // given 520 const text = `{ "version": 1 }`; 521 522 struct Value 523 { 524 int version_; 525 526 mixin(GenerateAll); 527 } 528 529 // when/then 530 text.decode!Value.should.equal(Value(1)); 531 } 532 533 @("const associative array") 534 unittest 535 { 536 // given 537 const text = ` { "key": "value" }`; 538 539 // when/then 540 text.decode!(const(string[string])).should.equal(["key": "value"]); 541 } 542 543 @("associative array in immutable struct") 544 unittest 545 { 546 // given 547 const text = `{ "entry": { "key": "value" } }`; 548 549 immutable struct Value 550 { 551 string[string] entry; 552 553 mixin(GenerateAll); 554 } 555 556 // when/then 557 text.decode!Value.should.equal(Value(["key": "value"])); 558 } 559 560 @("parsing is resumed") 561 unittest 562 { 563 import std.typecons : Nullable, nullable; 564 565 static struct FirstPass 566 { 567 string str; 568 569 int i; 570 571 ParserMarker value; 572 573 mixin(GenerateThis); 574 } 575 576 static struct Value 577 { 578 string message; 579 580 int[] array; 581 582 mixin(GenerateThis); 583 } 584 585 // given 586 const text = ` 587 { 588 "value": { 589 "message": "Hello World", 590 "array": [4, 5, 6] 591 }, 592 "str": "String", 593 "i": 5 594 } 595 `; 596 597 const firstPass = decode!FirstPass(text); 598 const secondPass = firstPass.value.decode!Value; 599 600 secondPass.should.equal(Value("Hello World", [4, 5, 6])); 601 } 602 603 @("associative array aliased to this in immutable struct") 604 unittest 605 { 606 // given 607 const text = `{ "entry": { "key": "value" } }`; 608 609 immutable struct Entry 610 { 611 string[string] entry; 612 613 alias entry this; 614 615 mixin(GenerateAll); 616 } 617 618 immutable struct Container 619 { 620 Entry entry; 621 622 mixin(GenerateAll); 623 } 624 625 // when/then 626 text.decode!Container.should.equal(Container(Entry(["key": "value"]))); 627 }