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 }