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 @("enums are decoded from strings") 148 unittest 149 { 150 enum Enum 151 { 152 A 153 } 154 155 struct Value 156 { 157 Enum field; 158 159 mixin(GenerateAll); 160 } 161 162 // given 163 const text = `{ "field": "A" }`; 164 165 // when 166 const value = decode!Value(text); 167 168 // then 169 const expected = Value(Enum.A); 170 171 value.should.equal(expected); 172 } 173 174 @("alias-this is decoded from inline keys") 175 unittest 176 { 177 struct A 178 { 179 int value2; 180 181 mixin(GenerateAll); 182 } 183 184 struct B 185 { 186 int value1; 187 188 A a; 189 190 alias a this; 191 192 mixin(GenerateAll); 193 } 194 195 // given 196 const text = `{ "value1": 3, "value2": 5 }`; 197 198 // when 199 const actual = decode!B(text); 200 201 // then 202 const expected = B(3, A(5)); 203 204 actual.should.equal(expected); 205 } 206 207 @("alias-this is decoded from inline keys for aliased methods") 208 unittest 209 { 210 struct A 211 { 212 int value2; 213 214 mixin(GenerateAll); 215 } 216 217 struct B 218 { 219 int value1; 220 221 @ConstRead 222 A a_; 223 224 mixin(GenerateAll); 225 226 alias a this; 227 } 228 229 // given 230 const text = `{ "value1": 3, "value2": 5 }`; 231 232 // when 233 const actual = decode!B(text); 234 235 // then 236 const expected = B(3, A(5)); 237 238 actual.should.equal(expected); 239 } 240 241 struct NestedValue 242 { 243 @(Json("Element")) 244 public string value; 245 246 mixin (GenerateAll); 247 } 248 249 struct Value 250 { 251 @(Json("IntValueElement")) 252 public int intValue; 253 254 @(Json("StringValueElement")) 255 public string stringValue; 256 257 @(Json("BoolValueElement")) 258 public bool boolValue; 259 260 @(Json("NestedElement")) 261 public NestedValue nestedValue; 262 263 @(Json("ArrayElement")) 264 public const int[] arrayValue; 265 266 @(Json("AssocArrayElement")) 267 public string[string] assocArray; 268 269 @(Json("DateElement")) 270 public Date dateValue; 271 272 @(Json("SysTimeElement")) 273 public SysTime sysTimeValue; 274 275 mixin (GenerateAll); 276 } 277 278 struct ValueWithDecoders 279 { 280 @(Json("asFoo")) 281 @(Json.Decode!fromFoo) 282 public string foo; 283 284 @(Json("asBar")) 285 @(Json.Decode!fromBar) 286 public string bar; 287 288 static string fromFoo(JSONValue value) 289 { 290 value.str.should.equal("foo"); 291 292 return "foobla"; 293 } 294 295 static string fromBar(JSONValue value) 296 { 297 value.str.should.equal("bar"); 298 299 return "barbla"; 300 } 301 302 mixin(GenerateThis); 303 } 304 305 @(Json.Decode!decodeTypeWithDecoder) 306 struct TypeWithDecoder 307 { 308 string value; 309 } 310 311 TypeWithDecoder decodeTypeWithDecoder(JSONValue value) 312 { 313 value.should.equal(JSONValue("bla")); 314 315 return TypeWithDecoder("123"); 316 } 317 318 @("transform functions may modify the values that are decoded") 319 unittest 320 { 321 import std.conv : to; 322 323 struct InnerDto 324 { 325 string encodedValue; 326 327 mixin(GenerateThis); 328 } 329 330 struct Inner 331 { 332 int value; 333 334 mixin(GenerateThis); 335 } 336 337 struct Struct 338 { 339 Inner inner; 340 341 mixin(GenerateThis); 342 } 343 344 alias transform(T : Inner) = (InnerDto innerDto) => 345 Inner(innerDto.encodedValue.to!int); 346 347 // !!! important to instantiate transform somewhere, to shake out errors 348 assert(transform!Inner(InnerDto("3")) == Inner(3)); 349 350 // given 351 const text = `{ "inner": { "encodedValue": "5" } }`; 352 353 // when 354 const actual = decode!(Struct, transform)(text); 355 356 // then 357 const expected = Struct(Inner(5)); 358 359 actual.should.equal(expected); 360 } 361 362 @("transform function with JSONValue parameter") 363 unittest 364 { 365 import std.conv : to; 366 367 struct Inner 368 { 369 int value; 370 371 mixin(GenerateThis); 372 } 373 374 struct Struct 375 { 376 Inner inner; 377 378 mixin(GenerateThis); 379 } 380 381 alias transform(T : Inner) = (JSONValue json) => 382 Inner(json.str.to!int); 383 384 // !!! important to instantiate transform somewhere, to shake out errors 385 assert(transform!Inner(JSONValue("3")) == Inner(3)); 386 387 // given 388 const text = `{ "inner": "5" }`; 389 390 // when 391 const actual = decode!(Struct, transform)(text); 392 393 // then 394 const expected = Struct(Inner(5)); 395 396 actual.should.equal(expected); 397 } 398 399 @("decode const array") 400 unittest 401 { 402 // given 403 const text = `[1, 2, 3]`; 404 405 // when 406 const actual = decode!(const(int[]))(text); 407 408 // Then 409 const expected = [1, 2, 3]; 410 411 actual.should.equal(expected); 412 } 413 414 @("missing fields") 415 unittest 416 { 417 // given 418 const text = `{}`; 419 420 struct S 421 { 422 int field; 423 424 mixin(GenerateThis); 425 } 426 427 // when/then 428 decode!S(text).should.throwA!JSONException("expected S.field, but got {}"); 429 } 430 431 @("struct with version_ field") 432 unittest 433 { 434 // given 435 const text = `{ "version": 1 }`; 436 437 struct Value 438 { 439 int version_; 440 441 mixin(GenerateAll); 442 } 443 444 // when/then 445 text.decode!Value.should.equal(Value(1)); 446 }