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 @("struct with version_ field")
517 unittest
518 {
519     // given
520     const text = `{ "version": 1 }`;
521 
522     struct Value
523     {
524         int version_;
525 
526         mixin(GenerateAll);
527     }
528 
529     // when/then
530     text.decode!Value.should.equal(Value(1));
531 }
532 
533 @("const associative array")
534 unittest
535 {
536     // given
537     const text = ` { "key": "value" }`;
538 
539     // when/then
540     text.decode!(const(string[string])).should.equal(["key": "value"]);
541 }
542 
543 @("associative array in immutable struct")
544 unittest
545 {
546     // given
547     const text = `{ "entry": { "key": "value" } }`;
548 
549     immutable struct Value
550     {
551         string[string] entry;
552 
553         mixin(GenerateAll);
554     }
555 
556     // when/then
557     text.decode!Value.should.equal(Value(["key": "value"]));
558 }
559 
560 @("parsing is resumed")
561 unittest
562 {
563     import std.typecons : Nullable, nullable;
564 
565     static struct FirstPass
566     {
567         string str;
568 
569         int i;
570 
571         ParserMarker value;
572 
573         mixin(GenerateThis);
574     }
575 
576     static struct Value
577     {
578         string message;
579 
580         int[] array;
581 
582         mixin(GenerateThis);
583     }
584 
585     // given
586     const text = `
587     {
588         "value": {
589             "message": "Hello World",
590             "array": [4, 5, 6]
591         },
592         "str": "String",
593         "i": 5
594     }
595     `;
596 
597     const firstPass = decode!FirstPass(text);
598     const secondPass = firstPass.value.decode!Value;
599 
600     secondPass.should.equal(Value("Hello World", [4, 5, 6]));
601 }
602 
603 @("associative array aliased to this in immutable struct")
604 unittest
605 {
606     // given
607     const text = `{ "entry": { "key": "value" } }`;
608 
609     immutable struct Entry
610     {
611         string[string] entry;
612 
613         alias entry this;
614 
615         mixin(GenerateAll);
616     }
617 
618     immutable struct Container
619     {
620         Entry entry;
621 
622         mixin(GenerateAll);
623     }
624 
625     // when/then
626     text.decode!Container.should.equal(Container(Entry(["key": "value"])));
627 }