1 /**
2   * Enables marking user-defined types for JSON serialization.
3   *
4   * Authors: <a href="https://github.com/rcorre">rcorre</a>
5 	* License: <a href="http://opensource.org/licenses/MIT">MIT</a>
6 	* Copyright: Copyright © 2015, rcorre
7   * Date: 3/23/15
8   */
9 module jsonizer.jsonize;
10 
11 import jsonizer.tojson : toJSON;
12 import jsonizer.fromjson : fromJSON;
13 import jsonizer.internal.util;
14 
15 public import jsonizer.internal.attribute;
16 
17 /// Generate json (de)serialization methods for the type this is mixed in to.
18 /// The methods `_toJSON` and `_fromJSON` are generated.
19 /// Params:
20 ///   ignoreExtra = whether to silently ignore json keys that do not map to serialized members
21 mixin template JsonizeMe(JsonizeIgnoreExtraKeys ignoreExtra = JsonizeIgnoreExtraKeys.yes) {
22   static import std.json;
23 
24   // Nested mixins -- these generate private functions to perform serialization/deserialization
25   private mixin template MakeDeserializer() {
26     private void _fromJSON(std.json.JSONValue json) {
27       // scoped imports include necessary functions without avoid polluting class namespace
28       import std.traits          : isNested, isAggregateType;
29       import std.algorithm       : filter;
30       import jsonizer.jsonize    : JsonizeIgnoreExtraKeys;
31       import jsonizer.fromjson   : fromJSON, nestedFromJSON, hasCustomJsonCtor;
32       import jsonizer.exceptions : JsonizeMismatchException;
33       import jsonizer.internal.util;
34 
35       alias T = typeof(this);
36 
37       // TODO: look into moving this up a level and not generating _fromJSON at all.
38       static if (!hasCustomJsonCtor!T) {
39         // track fields found to detect keys that do not map to serialized fields
40         int fieldsFound = 0;
41         string[] missingKeys;
42         auto keyValPairs = json.object;
43 
44         // check if each member is actually a member and is marked with the @jsonize attribute
45         foreach(member ; filteredMembers!T) {
46           // even with filtering members, need to make sure this is a valid member.
47           // Things like nested class types will make it through.
48           static if (__traits(compiles, __traits(getMember, this, member))) {
49             // find @jsonize, deduce member key
50             enum key = jsonizeKey!(__traits(getMember, this, member), member);
51           }
52           else {
53             enum key = null; // not a real member
54           }
55 
56           static if (key !is null) {
57             if (key in keyValPairs) {
58               ++fieldsFound;
59               alias MemberType = typeof(mixin(member));         // deduce member type
60               // special handling for nested class types
61               static if (isAggregateType!MemberType && isNested!MemberType) {
62                 auto val = nestedFromJSON!MemberType(keyValPairs[key], this);
63               }
64               else {
65                 auto val = fromJSON!MemberType(keyValPairs[key]); // extract value from json
66               }
67               mixin("this." ~ member ~ "= val;");               // assign value to member
68             }
69             else {
70               static if (!isOptional!(__traits(getMember, this, member))) {
71                 missingKeys ~= key;
72               }
73             }
74           }
75         }
76 
77         bool extraKeyFailure = false; // should we fail due to extra keys?
78         static if (ignoreExtra == JsonizeIgnoreExtraKeys.no) {
79           extraKeyFailure = (fieldsFound != keyValPairs.keys.length);
80         }
81 
82         // check for failure condition
83         // TODO: clean up with template to get all @jsonized members
84         if (extraKeyFailure || missingKeys.length > 0) {
85           string[] extraKeys;
86           foreach(jsonKey ; json.object.byKey) {
87             bool match = false;
88             foreach(member ; filteredMembers!T) {
89               static if (__traits(compiles, __traits(getMember, this, member))) {
90                 enum memberKey = jsonizeKey!(__traits(getMember, this, member), member);
91               }
92               else {
93                 enum memberKey = null;
94               }
95 
96               if (memberKey == jsonKey) {
97                 match = true;
98                 break;
99               }
100             }
101             if (!match) {
102               extraKeys ~= jsonKey;
103             }
104           }
105 
106           throw new JsonizeMismatchException(typeid(T), extraKeys, missingKeys);
107         }
108       }
109     }
110   }
111 
112   private mixin template MakeSerializer() {
113     private std.json.JSONValue _toJSON() {
114       import jsonizer.tojson        : toJSON;
115       import jsonizer.internal.util : jsonizeKey, filteredMembers;
116 
117       alias T = typeof(this);
118 
119       std.json.JSONValue[string] keyValPairs;
120       // look for members marked with @jsonize, ignore __ctor
121       foreach(member ; filteredMembers!T) {
122         // find @jsonize, deduce member key
123         static if (__traits(compiles, __traits(getMember, this, member))) {
124           enum key = jsonizeKey!(__traits(getMember, this, member), member);
125         }
126         else {
127           enum key = null;
128         }
129 
130         static if(key !is null) {
131           auto val = mixin("this." ~ member); // get the member's value
132           keyValPairs[key] = toJSON(val);     // add the pair <memberKey> : <memberValue>
133         }
134       }
135       // construct the json object
136       std.json.JSONValue json;
137       json.object = keyValPairs;
138       return json;
139     }
140   }
141 
142   // generate private functions with no override specifiers
143   mixin MakeSerializer GeneratedSerializer;
144   mixin MakeDeserializer GeneratedDeserializer;
145 
146   // expose the methods generated above by wrapping them in public methods.
147   // apply the overload attribute to the public methods if already implemented in base class.
148   static if (is(typeof(this) == class) &&
149       __traits(hasMember, std.traits.BaseClassesTuple!(typeof(this))[0], "populateFromJSON"))
150   {
151     override void populateFromJSON(std.json.JSONValue json) {
152       GeneratedDeserializer._fromJSON(json);
153     }
154 
155     override std.json.JSONValue convertToJSON() {
156       return GeneratedSerializer._toJSON();
157     }
158   }
159   else {
160     void populateFromJSON(std.json.JSONValue json) {
161       GeneratedDeserializer._fromJSON(json);
162     }
163 
164     std.json.JSONValue convertToJSON() {
165       return GeneratedSerializer._toJSON();
166     }
167   }
168 }
169 
170 /// object serialization -- fields only
171 unittest {
172   import std.math : approxEqual;
173 
174   static class Fields {
175     this() { } // class must have a no-args ctor
176 
177     this(int iVal, float fVal, string sVal, int[] aVal, string noJson) {
178       i = iVal;
179       f = fVal;
180       s = sVal;
181       a = aVal;
182       dontJsonMe = noJson;
183     }
184 
185     mixin JsonizeMe;
186 
187     @jsonize { // fields to jsonize -- test different access levels
188       public int i;
189       protected float f;
190       public int[] a;
191       private string s;
192     }
193     string dontJsonMe;
194 
195     override bool opEquals(Object o) {
196       auto other = cast(Fields) o;
197       return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f);
198     }
199   }
200 
201   auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg");
202   auto json = toJSON!Fields(obj);
203 
204   assert(json.object["i"].integer == 1);
205   assert(json.object["f"].floating.approxEqual(4.2));
206   assert(json.object["s"].str == "tally ho!");
207   assert(json.object["a"].array[0].integer == 9);
208   assert("dontJsonMe" !in json.object);
209 
210   // reconstruct from json
211   auto r = fromJSON!Fields(json);
212   assert(r.i == 1);
213   assert(r.f.approxEqual(4.2));
214   assert(r.s == "tally ho!");
215   assert(r.a == [9, 8, 7, 6]);
216   assert(r.dontJsonMe is null);
217 
218   // array of objects
219   auto a = [
220     new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"),
221         new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus")
222   ];
223 
224   // serialize array of user objects to json
225   auto jsonArray = toJSON!(Fields[])(a);
226   // reconstruct from json
227   assert(fromJSON!(Fields[])(jsonArray) == a);
228 }
229 
230 /// object serialization with properties
231 unittest {
232   import std.math : approxEqual;
233 
234   static class Props {
235     this() { } // class must have a no-args ctor
236 
237     this(int iVal, float fVal, string sVal, string noJson) {
238       _i = iVal;
239       _f = fVal;
240       _s = sVal;
241       _dontJsonMe = noJson;
242     }
243 
244     mixin JsonizeMe;
245 
246     @property {
247       // jsonize ref property accessing private field
248       @jsonize ref int i() { return _i; }
249       // jsonize property with non-trivial get/set methods
250       @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3
251       float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json
252       // don't jsonize these properties
253       ref string s() { return _s; }
254       ref string dontJsonMe() { return _dontJsonMe; }
255     }
256 
257     private:
258     int _i;
259     float _f;
260     @jsonize string _s;
261     string _dontJsonMe;
262   }
263 
264   auto obj = new Props(1, 4.2, "tally ho!", "blarg");
265   auto json = toJSON(obj);
266 
267   assert(json.object["i"].integer == 1);
268   assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval
269   assert(json.object["_s"].str == "tally ho!");
270   assert("dontJsonMe" !in json.object);
271 
272   auto r = fromJSON!Props(json);
273   assert(r.i == 1);
274   assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5
275   assert(r._s == "tally ho!");
276   assert(r.dontJsonMe is null);
277 }
278 
279 /// object serialization with custom constructor
280 unittest {
281   import std.conv : to;
282   import std.json : parseJSON;
283   import std.math : approxEqual;
284   import jsonizer.tojson : toJSON;
285 
286   static class Custom {
287     mixin JsonizeMe;
288 
289     this(int i) {
290       _i = i;
291       _s = "something";
292       _f = 10.2;
293     }
294 
295     @jsonize this(int _i, string _s, float _f = 20.2) {
296       this._i = _i;
297       this._s = _s ~ " jsonized";
298       this._f = _f;
299     }
300 
301     @jsonize this(double d) { // alternate ctor
302       _f = d.to!float;
303       _s = d.to!string;
304       _i = d.to!int;
305     }
306 
307     private:
308     @jsonize {
309       string _s;
310       float _f;
311       int _i;
312     }
313   }
314 
315   auto c = new Custom(12);
316   auto json = toJSON(c);
317   assert(json.object["_i"].integer == 12);
318   assert(json.object["_s"].str == "something");
319   assert(json.object["_f"].floating.approxEqual(10.2));
320   auto c2 = fromJSON!Custom(json);
321   assert(c2._i == 12);
322   assert(c2._s == "something jsonized");
323   assert(c2._f.approxEqual(10.2));
324 
325   // test alternate ctor
326   json = parseJSON(`{"d" : 5}`);
327   c = json.fromJSON!Custom;
328   assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5");
329 }
330 
331 /// struct serialization
332 unittest {
333   import std.math : approxEqual;
334 
335   static struct S {
336     mixin JsonizeMe;
337 
338     @jsonize {
339       int x;
340       float f;
341       string s;
342     }
343     int dontJsonMe;
344 
345     this(int x, float f, string s, int noJson) {
346       this.x = x;
347       this.f = f;
348       this.s = s;
349       this.dontJsonMe = noJson;
350     }
351   }
352 
353   auto s = S(5, 4.2, "bogus", 7);
354   auto json = toJSON(s); // serialize a struct
355 
356   assert(json.object["x"].integer == 5);
357   assert(json.object["f"].floating.approxEqual(4.2));
358   assert(json.object["s"].str == "bogus");
359   assert("dontJsonMe" !in json.object);
360 
361   auto r = fromJSON!S(json);
362   assert(r.x == 5);
363   assert(r.f.approxEqual(4.2));
364   assert(r.s == "bogus");
365   assert(r.dontJsonMe == int.init);
366 }
367 
368 /// json file I/O
369 unittest {
370   import std.file          : remove;
371   import jsonizer.fromjson : readJSON;
372   import jsonizer.tojson   : writeJSON;
373 
374   enum file = "test.json";
375   scope(exit) remove(file);
376 
377   static struct Data {
378     mixin JsonizeMe;
379 
380     @jsonize {
381       int x;
382       string s;
383       float f;
384     }
385   }
386 
387   // write an array of user-defined structs
388   auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)];
389   file.writeJSON(array);
390   auto readBack = file.readJSON!(Data[]);
391   assert(readBack == array);
392 
393   // now try an associative array
394   auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)];
395   file.writeJSON(aa);
396   auto aaReadBack = file.readJSON!(Data[string]);
397   assert(aaReadBack == aa);
398 }
399 
400 /// inheritance
401 unittest {
402   import std.math : approxEqual;
403   static class Parent {
404     mixin JsonizeMe;
405     @jsonize {
406       int x;
407       string s;
408     }
409   }
410 
411   static class Child : Parent {
412     mixin JsonizeMe;
413     @jsonize {
414       float f;
415     }
416   }
417 
418   auto c = new Child;
419   c.x = 5;
420   c.s = "hello";
421   c.f = 2.1;
422 
423   auto json = c.toJSON;
424   assert(json.fromJSON!int("x") == 5);
425   assert(json.fromJSON!string("s") == "hello");
426   assert(json.fromJSON!float("f").approxEqual(2.1));
427 
428   auto child = json.fromJSON!Child;
429   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
430 
431   auto parent = json.fromJSON!Parent;
432   assert(parent.x == 5 && parent.s == "hello");
433 }
434 
435 /// inheritance with  ctors
436 unittest {
437   import std.math : approxEqual;
438   static class Parent {
439     mixin JsonizeMe;
440 
441     @jsonize this(int x, string s) {
442       _x = x;
443       _s = s;
444     }
445 
446     @jsonize @property {
447       int x()    { return _x; }
448       string s() { return _s; }
449     }
450 
451     private:
452     int _x;
453     string _s;
454   }
455 
456   static class Child : Parent {
457     mixin JsonizeMe;
458 
459     @jsonize this(int x, string s, float f) {
460       super(x, s);
461       _f = f;
462     }
463 
464     @jsonize @property {
465       float f() { return _f; }
466     }
467 
468     private:
469     float _f;
470   }
471 
472   auto c = new Child(5, "hello", 2.1);
473 
474   auto json = c.toJSON;
475   assert(json.fromJSON!int("x") == 5);
476   assert(json.fromJSON!string("s") == "hello");
477   assert(json.fromJSON!float("f").approxEqual(2.1));
478 
479   auto child = json.fromJSON!Child;
480   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
481 
482   auto parent = json.fromJSON!Parent;
483   assert(parent.x == 5 && parent.s == "hello");
484 }
485 
486 /// renamed members
487 unittest {
488   static class Bleh {
489     mixin JsonizeMe;
490     private {
491       @jsonize("x") int _x;
492       @jsonize("s") string _s;
493     }
494   }
495 
496   auto b = new Bleh;
497   b._x = 5;
498   b._s = "blah";
499 
500   auto json = b.toJSON;
501 
502   assert(json.fromJSON!int("x") == 5);
503   assert(json.fromJSON!string("s") == "blah");
504 
505   auto reconstruct = json.fromJSON!Bleh;
506   assert(reconstruct._x == b._x && reconstruct._s == b._s);
507 }
508 
509 // members that potentially conflict with variables used in the mixin
510 unittest {
511   static struct Foo {
512     mixin JsonizeMe;
513     @jsonize int val;
514   }
515 
516   Foo orig = Foo(3);
517   auto serialized   = orig.toJSON;
518   auto deserialized = serialized.fromJSON!Foo;
519 
520   assert(deserialized.val == orig.val);
521 }
522 
523 /// unfortunately these test classes must be implemented outside the unittest
524 /// as Object.factory (and ClassInfo.find) cannot work with nested classes
525 private {
526   class TestComponent {
527     mixin JsonizeMe;
528     @jsonize int c;
529   }
530 
531   class TestCompA : TestComponent {
532     mixin JsonizeMe;
533     @jsonize int a;
534   }
535 
536   class TestCompB : TestComponent {
537     mixin JsonizeMe;
538     @jsonize string b;
539   }
540 }
541 
542 /// type inference
543 unittest {
544   import std.json   : parseJSON;
545   import std.string : format;
546   import std.traits : fullyQualifiedName;
547 
548   // need to use these because unittest is assigned weird name
549   // normally would just be "modulename.classname"
550   string classKeyA = fullyQualifiedName!TestCompA;
551   string classKeyB = fullyQualifiedName!TestCompB;
552 
553   assert(Object.factory(classKeyA) !is null && Object.factory(classKeyB) !is null,
554       "cannot factory classes in unittest -- this is a problem with the test");
555 
556   auto data = `[
557     {
558       "class": "%s",
559       "c": 1,
560       "a": 5
561     },
562     {
563       "class": "%s",
564       "c": 2,
565       "b": "hello"
566     }
567   ]`.format(classKeyA, classKeyB).parseJSON.fromJSON!(TestComponent[]);
568 
569   auto a = cast(TestCompA) data[0];
570   auto b = cast(TestCompB) data[1];
571 
572   assert(a !is null && a.c == 1 && a.a == 5);
573   assert(b !is null && b.c == 2 && b.b == "hello");
574 }
575 
576 // TODO: These are not examples but edge-case tests
577 // factor out into dedicated test modules
578 
579 // Validate issue #20:
580 // Unable to de-jsonize a class when a construct is marked @jsonize.
581 unittest {
582   import std.json            : parseJSON;
583   import std.algorithm       : canFind;
584   import std.exception       : collectException;
585   import jsonizer.jsonize    : jsonize, JsonizeMe;
586   import jsonizer.exceptions : JsonizeConstructorException;
587   import jsonizer.fromjson   : fromJSON;
588 
589   static class A {
590     private const int a;
591 
592     this(float f) {
593       a = 0;
594     }
595 
596     @jsonize this(int a) {
597       this.a = a;
598     }
599 
600     @jsonize this(string s, float f) {
601       a = 0;
602     }
603   }
604 
605   auto ex = collectException!JsonizeConstructorException(`{}`.parseJSON.fromJSON!A);
606   assert(ex !is null, "failure to match @jsonize'd constructors should throw");
607   assert(ex.msg.canFind("(int a)") && ex.msg.canFind("(string s, float f)"),
608     "JsonizeConstructorException message should contain attempted constructors");
609   assert(!ex.msg.canFind("(float f)"),
610     "JsonizeConstructorException message should not contain non-jsonized constructors");
611 }
612 
613 // Validate issue #17:
614 // Unable to construct class containing private (not marked with @jsonize) types.
615 unittest {
616   import std.json : parseJSON;
617 
618   static class A {
619     mixin JsonizeMe;
620 
621     private int a;
622 
623     @jsonize public this(int a) {
624         this.a = a;
625     }
626   }
627 
628   auto json = `{ "a": 5}`.parseJSON;
629   auto a = fromJSON!A(json);
630 
631   assert(a.a == 5);
632 }
633 
634 // Validate issue #18:
635 // Unable to construct class with const types.
636 unittest {
637   import std.json : parseJSON;
638 
639   static class A {
640     mixin JsonizeMe;
641 
642     const int a;
643 
644     @jsonize public this(int a) {
645         this.a = a;
646     }
647   }
648 
649   auto json = `{ "a": 5}`.parseJSON;
650   auto a = fromJSON!A(json);
651 
652   assert(a.a == 5);
653 }
654 
655 // Validate issue #19:
656 // Unable to construct class containing private (not marked with @jsonize) types.
657 unittest {
658   import std.json : parseJSON;
659 
660   static class A {
661     mixin JsonizeMe;
662 
663     alias Integer = int;
664     Integer a;
665 
666     @jsonize public this(Integer a) {
667         this.a = a;
668     }
669   }
670 
671   auto json = `{ "a": 5}`.parseJSON;
672   auto a = fromJSON!A(json);
673 
674   assert(a.a == 5);
675 }