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 }