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