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 @("decode null object")
517 unittest
518 {
519     // given
520     const text = `null`;
521 
522     static class Value
523     {
524         mixin(GenerateAll);
525     }
526 
527     // when/then
528     decode!Value(text).should.be(null);
529 }
530 
531 @("struct with version_ field")
532 unittest
533 {
534     // given
535     const text = `{ "version": 1 }`;
536 
537     struct Value
538     {
539         int version_;
540 
541         mixin(GenerateAll);
542     }
543 
544     // when/then
545     text.decode!Value.should.equal(Value(1));
546 }
547 
548 @("const associative array")
549 unittest
550 {
551     // given
552     const text = ` { "key": "value" }`;
553 
554     // when/then
555     text.decode!(const(string[string])).should.equal(["key": "value"]);
556 }
557 
558 @("associative array in immutable struct")
559 unittest
560 {
561     // given
562     const text = `{ "entry": { "key": "value" } }`;
563 
564     immutable struct Value
565     {
566         string[string] entry;
567 
568         mixin(GenerateAll);
569     }
570 
571     // when/then
572     text.decode!Value.should.equal(Value(["key": "value"]));
573 }
574 
575 @("parsing is resumed")
576 unittest
577 {
578     import std.typecons : Nullable, nullable;
579 
580     static immutable struct FirstPass
581     {
582         string str;
583 
584         int i;
585 
586         ParserMarker value;
587 
588         mixin(GenerateThis);
589     }
590 
591     static struct Value
592     {
593         string message;
594 
595         int[] array;
596 
597         mixin(GenerateThis);
598     }
599 
600     // given
601     const text = `
602     {
603         "value": {
604             "message": "Hello World",
605             "array": [4, 5, 6]
606         },
607         "str": "String",
608         "i": 5
609     }
610     `;
611 
612     const firstPass = decode!FirstPass(text);
613     const secondPass = firstPass.value.decode!Value;
614 
615     secondPass.should.equal(Value("Hello World", [4, 5, 6]));
616 }
617 
618 @("associative array aliased to this in immutable struct")
619 unittest
620 {
621     // given
622     const text = `{ "entry": { "key": "value" } }`;
623 
624     immutable struct Entry
625     {
626         string[string] entry;
627 
628         alias entry this;
629 
630         mixin(GenerateAll);
631     }
632 
633     immutable struct Container
634     {
635         Entry entry;
636 
637         mixin(GenerateAll);
638     }
639 
640     // when/then
641     text.decode!Container.should.equal(Container(Entry(["key": "value"])));
642 }
643 
644 @("type with invariant")
645 unittest
646 {
647     // given
648     const text = `{ "a": 2, "b": 3 }`;
649 
650     static struct Value
651     {
652         int a;
653 
654         int b;
655 
656         invariant (this.a == this.b);
657 
658         mixin(GenerateAll);
659     }
660 
661     // when/then
662     text.decode!Value.should.throwA!JSONException(
663         "unittest/text/json/DecodeTest.d:656 - while decoding Value: Assertion failure");
664 }