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 }