1 module text.json.EncodeTest;
2 
3 import boilerplate;
4 import dshould;
5 import std.datetime;
6 import std.json;
7 import text.json.Encode;
8 import text.json.Json;
9 
10 // All the tests are executed with both `encodeJson` (encodes to a JSONValue)
11 // and `encode` (encodes to a string).
12 static foreach (bool useEncodeJson; [false, true])
13 {
14     mixin encodeTests!(useEncodeJson);
15 }
16 
17 template encodeTests(bool useEncodeJson)
18 {
19     static if (useEncodeJson)
20     {
21         enum prefix = "encodeJson:";
22 
23         template testEncode(Args...)
24         {
25             JSONValue testEncode(T)(const T value)
26             {
27                 return value.encodeJson!(Args);
28             }
29         }
30     }
31     else
32     {
33         enum prefix = "encode:";
34 
35         template testEncode(Args...)
36         {
37             JSONValue testEncode(T)(const T value)
38             {
39                 return value.encode!(Args).parseJSON;
40             }
41         }
42     }
43 
44     @(prefix ~ "aggregate types are encoded to JSON text")
45     unittest
46     {
47         // given
48         const value = (){
49             import text.time.Convert : Convert;
50 
51             with (Value.Builder())
52             {
53                 intValue = 23;
54                 stringValue = "FOO";
55                 boolValue = true;
56                 nestedValue = NestedValue("Bar");
57                 arrayValue = [1, 2, 3];
58                 assocArray = ["foo": NestedValue("bar"), "baz": NestedValue("whee")];
59                 nestedArray = [NestedValue("Foo"), NestedValue("Bar")];
60                 dateValue = Date(2000, 1, 2);
61                 sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z");
62                 return value;
63             }
64         }();
65 
66         // when
67         const actual = testEncode(value);
68 
69         const expected = `
70         {
71             "IntValueElement": 23,
72             "StringValueElement": "FOO",
73             "BoolValueElement": true,
74             "NestedElement": {
75                 "Element": "Bar"
76             },
77             "ArrayElement": [1, 2, 3],
78             "AssocArrayElement": {
79                 "baz": { "Element": "whee" },
80                 "foo": { "Element": "bar" }
81             },
82             "NestedArray": [
83                 { "Element": "Foo" },
84                 { "Element": "Bar" }
85             ],
86             "DateElement": "2000-01-02",
87             "SysTimeElement": "2000-01-02T10:00:00Z"
88         }
89         `.parseJSON;
90 
91         // then
92         actual.should.equal(expected);
93     }
94 
95     @(prefix ~ "custom encoders are used on fields")
96     unittest
97     {
98         // given
99         const value = ValueWithEncoders("bla", "bla");
100 
101         // when
102         auto actual = testEncode(value);
103 
104         // then
105         const expected = `{ "asFoo": "foo", "asBar": "bar" }`.parseJSON;
106 
107         actual.should.equal(expected);
108     }
109 
110     @(prefix ~ "custom encoders are used on a type")
111     unittest
112     {
113         // given
114         struct Value
115         {
116             TypeWithEncoder field;
117 
118             mixin(GenerateAll);
119         }
120 
121         const value = Value(TypeWithEncoder());
122 
123         // when
124         auto actual = testEncode(value);
125 
126         // then
127         const expected = `{ "field": "123" }`.parseJSON;
128 
129         actual.should.equal(expected);
130     }
131 
132     @(prefix ~ "enums are encoded as strings")
133     unittest
134     {
135         enum Enum
136         {
137             A
138         }
139 
140         struct Value
141         {
142             Enum field;
143 
144             mixin(GenerateAll);
145         }
146 
147         // given
148         const value = Value(Enum.A);
149 
150         // when
151         const actual = testEncode(value);
152 
153         // then
154         const expected = `{ "field": "A" }`.parseJSON;
155 
156         actual.should.equal(expected);
157     }
158 
159     @(prefix ~ "alias-this is encoded inline")
160     unittest
161     {
162         struct A
163         {
164             int value2;
165 
166             mixin(GenerateAll);
167         }
168 
169         struct B
170         {
171             int value1;
172 
173             A a;
174 
175             alias a this;
176 
177             mixin(GenerateAll);
178         }
179 
180         // given
181         const value = B(3, A(5));
182 
183         // when
184         const actual = testEncode(value);
185 
186         // then
187         const expected = `{ "value1": 3, "value2": 5 }`.parseJSON;
188 
189         actual.should.equal(expected);
190     }
191 
192     @(prefix ~ "alias-this is encoded inline for aliased methods")
193     unittest
194     {
195         struct A
196         {
197             int value2;
198 
199             mixin(GenerateAll);
200         }
201 
202         struct B
203         {
204             int value1;
205 
206             @ConstRead
207             A a_;
208 
209             mixin(GenerateAll);
210 
211             alias a this;
212         }
213 
214         // given
215         const value = B(3, A(5));
216 
217         // when
218         const actual = testEncode(value);
219 
220         // then
221         const expected = `{ "value1": 3, "value2": 5 }`.parseJSON;
222 
223         actual.should.equal(expected);
224     }
225 
226     @(prefix ~ "arrays of enums are encoded as strings")
227     unittest
228     {
229         enum Enum
230         {
231             A,
232         }
233 
234         struct Value
235         {
236             Enum[] value;
237 
238             mixin(GenerateAll);
239         }
240 
241         // given
242         const value = Value([Enum.A]);
243 
244         // when
245         auto actual = testEncode(value);
246 
247         // then
248         const expected = `{ "value": ["A"] }`.parseJSON;
249 
250         actual.should.equal(expected);
251     }
252 
253     @(prefix ~ "transform functions may modify the values that are encoded")
254     unittest
255     {
256         import std.conv : to;
257 
258         struct Inner
259         {
260             int value;
261 
262             mixin(GenerateThis);
263         }
264 
265         struct InnerDto
266         {
267             string encodedValue;
268 
269             mixin(GenerateThis);
270         }
271 
272         struct Struct
273         {
274             Inner inner;
275 
276             mixin(GenerateThis);
277         }
278 
279         InnerDto transform(Inner inner)
280         {
281             return InnerDto(inner.value.to!string);
282         }
283 
284         // given
285         const value = Struct(Inner(5));
286 
287         // when
288         const actual = testEncode!(Struct, transform)(value);
289 
290         // then
291         const expected = `{ "inner": { "encodedValue": "5" } }`.parseJSON;
292 
293         actual.should.equal(expected);
294     }
295 
296     @(prefix ~ "transform functions returning JSONValue")
297     unittest
298     {
299         import std.conv : to;
300 
301         struct Inner
302         {
303             int value;
304 
305             mixin(GenerateThis);
306         }
307 
308         struct Struct
309         {
310             Inner inner;
311 
312             mixin(GenerateThis);
313         }
314 
315         JSONValue transform(Inner inner)
316         {
317             return JSONValue(inner.value.to!string);
318         }
319 
320         // given
321         const value = Struct(Inner(5));
322 
323         // when
324         const actual = testEncode!(Struct, transform)(value);
325 
326         // then
327         const expected = `{ "inner": "5" }`.parseJSON;
328 
329         actual.should.equal(expected);
330     }
331 
332     @(prefix ~ "struct with version_ field")
333     unittest
334     {
335         // given
336         struct Value
337         {
338             int version_;
339 
340             mixin(GenerateAll);
341         }
342 
343         const value = Value(1);
344 
345         // when
346         auto actual = testEncode(value);
347 
348         // then
349         const expected = `{ "version": 1 }`.parseJSON;
350 
351         actual.should.equal(expected);
352     }
353 
354     @(prefix ~ "encode class")
355     unittest
356     {
357         // given
358         class Value
359         {
360             int field;
361 
362             mixin(GenerateAll);
363         }
364 
365         const value = new Value(1);
366 
367         // when
368         auto actual = testEncode(value);
369 
370         // then
371         const expected = `{ "field": 1 }`.parseJSON;
372 
373         actual.should.equal(expected);
374     }
375 
376     @(prefix ~ "encode null object")
377     unittest
378     {
379         // given
380         class Value
381         {
382             mixin(GenerateAll);
383         }
384 
385         // when
386         const actual = testEncode!Value(null);
387 
388         // then
389         enum expected = `null`.parseJSON;
390 
391         actual.should.equal(expected);
392     }
393 
394     @(prefix ~ "encode null object with transform")
395     unittest
396     {
397         import std.conv : to;
398 
399         JSONValue transform(const Object obj)
400         {
401             if (obj is null)
402             {
403                 return JSONValue(null);
404             }
405             assert(false);
406         }
407 
408         // given
409         const Object value = null;
410 
411         // when
412         const actual = testEncode!(Object, transform)(value);
413 
414         // then
415         enum expected = `null`.parseJSON;
416 
417         actual.should.equal(expected);
418     }
419 
420     static if (__traits(compiles, { import std.sumtype; }))
421     {
422         @(prefix ~ "encode std.sumtype")
423         unittest
424         {
425             import std.sumtype : SumType;
426 
427             // given
428             alias S = SumType!(int, string);
429 
430             const value = [S(1), S("foo")];
431 
432             // when
433             auto actual = testEncode(value);
434 
435             // then
436             const expected = `[1, "foo"]`.parseJSON;
437 
438             actual.should.equal(expected);
439         }
440 
441         @(prefix ~ "encode std.sumtype with transform function")
442         unittest
443         {
444             import std.sumtype : match, SumType;
445 
446             // given
447             alias S = SumType!(int, string);
448 
449             string transform(const S s)
450             {
451                 return s.match!((int i) => "int", (string s) => "string");
452             }
453 
454             const value = [S(1), S("foo")];
455 
456             // when
457             auto actual = testEncode!(S[], transform)(value);
458 
459             // then
460             enum expected = `["int", "string"]`.parseJSON;
461 
462             actual.should.equal(expected);
463         }
464     }
465 }
466 
467 struct NestedValue
468 {
469     @(Json("Element"))
470     public string value;
471 
472     mixin (GenerateAll);
473 }
474 
475 struct Value
476 {
477     @(Json("IntValueElement"))
478     public int intValue;
479 
480     @(Json("StringValueElement"))
481     public string stringValue;
482 
483     @(Json("BoolValueElement"))
484     public bool boolValue;
485 
486     @(Json("NestedElement"))
487     public NestedValue nestedValue;
488 
489     @(Json("ArrayElement"))
490     public const int[] arrayValue;
491 
492     @(Json("AssocArrayElement"))
493     public NestedValue[string] assocArray;
494 
495     @(Json("NestedArray"))
496     public NestedValue[] nestedArray;
497 
498     @(Json("DateElement"))
499     public Date dateValue;
500 
501     @(Json("SysTimeElement"))
502     public SysTime sysTimeValue;
503 
504     mixin (GenerateAll);
505 }
506 
507 struct ValueWithEncoders
508 {
509     @(Json("asFoo"))
510     @(Json.Encode!asFoo)
511     public string foo;
512 
513     @(Json("asBar"))
514     @(Json.Encode!asBar)
515     public string bar;
516 
517     static JSONValue asFoo(string field)
518     {
519         field.should.equal("bla");
520 
521         return JSONValue("foo");
522     }
523 
524     static JSONValue asBar(string field)
525     {
526         field.should.equal("bla");
527 
528         return JSONValue("bar");
529     }
530 
531     mixin(GenerateThis);
532 }
533 
534 @(Json.Encode!encodeTypeWithEncoder)
535 struct TypeWithEncoder
536 {
537 }
538 
539 JSONValue encodeTypeWithEncoder(TypeWithEncoder)
540 {
541     return JSONValue("123");
542 }