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 struct NestedValue 266 { 267 @(Json("Element")) 268 public string value; 269 270 mixin (GenerateAll); 271 } 272 273 struct Value 274 { 275 @(Json("IntValueElement")) 276 public int intValue; 277 278 @(Json("StringValueElement")) 279 public string stringValue; 280 281 @(Json("BoolValueElement")) 282 public bool boolValue; 283 284 @(Json("NestedElement")) 285 public NestedValue nestedValue; 286 287 @(Json("ArrayElement")) 288 public const int[] arrayValue; 289 290 @(Json("AssocArrayElement")) 291 public string[string] assocArray; 292 293 @(Json("DateElement")) 294 public Date dateValue; 295 296 @(Json("SysTimeElement")) 297 public SysTime sysTimeValue; 298 299 mixin (GenerateAll); 300 } 301 302 struct ValueWithDecoders 303 { 304 @(Json("asFoo")) 305 @(Json.Decode!fromFoo) 306 public string foo; 307 308 @(Json("asBar")) 309 @(Json.Decode!fromBar) 310 public string bar; 311 312 static string fromFoo(JSONValue value) 313 { 314 value.str.should.equal("foo"); 315 316 return "foobla"; 317 } 318 319 static string fromBar(JSONValue value) 320 { 321 value.str.should.equal("bar"); 322 323 return "barbla"; 324 } 325 326 mixin(GenerateThis); 327 } 328 329 @(Json.Decode!decodeTypeWithDecoder) 330 struct TypeWithDecoder 331 { 332 string value; 333 } 334 335 TypeWithDecoder decodeTypeWithDecoder(JSONValue value) 336 { 337 value.should.equal(JSONValue("bla")); 338 339 return TypeWithDecoder("123"); 340 } 341 342 @("transform functions may modify the values that are decoded") 343 unittest 344 { 345 import std.conv : to; 346 347 struct InnerDto 348 { 349 string encodedValue; 350 351 mixin(GenerateThis); 352 } 353 354 struct Inner 355 { 356 int value; 357 358 mixin(GenerateThis); 359 } 360 361 struct Struct 362 { 363 Inner inner; 364 365 mixin(GenerateThis); 366 } 367 368 alias transform(T : Inner) = (InnerDto innerDto) => 369 Inner(innerDto.encodedValue.to!int); 370 371 // !!! important to instantiate transform somewhere, to shake out errors 372 assert(transform!Inner(InnerDto("3")) == Inner(3)); 373 374 // given 375 const text = `{ "inner": { "encodedValue": "5" } }`; 376 377 // when 378 const actual = decode!(Struct, transform)(text); 379 380 // then 381 const expected = Struct(Inner(5)); 382 383 actual.should.equal(expected); 384 } 385 386 @("transform function with JSONValue parameter") 387 unittest 388 { 389 import std.conv : to; 390 391 struct Inner 392 { 393 int value; 394 395 mixin(GenerateThis); 396 } 397 398 struct Struct 399 { 400 Inner inner; 401 402 mixin(GenerateThis); 403 } 404 405 alias transform(T : Inner) = (JSONValue json) => 406 Inner(json.str.to!int); 407 408 // !!! important to instantiate transform somewhere, to shake out errors 409 assert(transform!Inner(JSONValue("3")) == Inner(3)); 410 411 // given 412 const text = `{ "inner": "5" }`; 413 414 // when 415 const actual = decode!(Struct, transform)(text); 416 417 // then 418 const expected = Struct(Inner(5)); 419 420 actual.should.equal(expected); 421 } 422 423 @("decode const array") 424 unittest 425 { 426 // given 427 const text = `[1, 2, 3]`; 428 429 // when 430 const actual = decode!(const(int[]))(text); 431 432 // Then 433 const expected = [1, 2, 3]; 434 435 actual.should.equal(expected); 436 } 437 438 @("missing fields") 439 unittest 440 { 441 // given 442 const text = `{}`; 443 444 struct S 445 { 446 int field; 447 448 mixin(GenerateThis); 449 } 450 451 // when/then 452 decode!S(text).should.throwA!JSONException("expected S.field, but got {}"); 453 } 454 455 @("struct with version_ field") 456 unittest 457 { 458 // given 459 const text = `{ "version": 1 }`; 460 461 struct Value 462 { 463 int version_; 464 465 mixin(GenerateAll); 466 } 467 468 // when/then 469 text.decode!Value.should.equal(Value(1)); 470 } 471 472 @("array of structs") 473 unittest 474 { 475 static struct S 476 { 477 int field; 478 479 mixin(GenerateThis); 480 } 481 // given 482 const text = `[{ "field": 1 }]`; 483 484 // when/then 485 text.decode!(S[]).should.equal([S(1)]); 486 }