1 /// serialize and deserialize between JSONValues and other D types
2 module jsonizer;
3 
4 import std.json;
5 import std.file;
6 import std.conv;
7 import std.range;
8 import std.traits;
9 import std.string;
10 import std.algorithm;
11 import std.exception;
12 import std.typetuple;
13 import std.typecons : staticIota;
14 
15 public import internal.io;
16 public import internal.tojson;
17 public import internal.extract;
18 public import internal.attribute;
19 
20 
21 string jsonizeKey(alias obj, string memberName)() {
22   foreach(attr ; __traits(getAttributes, mixin("obj." ~ memberName))) {
23     static if (is(attr == jsonize)) { // @jsonize someMember;
24       return memberName;          // use member name as-is
25     }
26     else static if (is(typeof(attr) == jsonize)) { // @jsonize("someKey") someMember;
27       return attr.key;
28     }
29   }
30   return null;
31 } 
32 
33 mixin template JsonizeMe() {
34   import std.json      : JSONValue;
35   import std.typetuple : Erase;
36   import std.traits    : BaseClassesTuple;
37 
38   alias T = typeof(this);
39   static if (is(T == class) && 
40       __traits(hasMember, BaseClassesTuple!T[0], "populateFromJSON")) 
41   {
42     override void populateFromJSON(JSONValue json) {
43       static if (!hasCustomJsonCtor!T) {
44         auto keyValPairs = json.object;
45         // check if each member is actually a member and is marked with the @jsonize attribute
46         foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
47           enum key = jsonizeKey!(this, member);              // find @jsonize, deduce member key
48           static if (key !is null) {
49             alias MemberType = typeof(mixin(member));        // deduce member type
50             auto val = extract!MemberType(keyValPairs[key]); // extract value from json
51             mixin(member ~ "= val;");                        // assign value to member
52           }
53         }
54       }
55     }
56 
57     override JSONValue convertToJSON() {
58       JSONValue[string] keyValPairs;
59       // look for members marked with @jsonize, ignore __ctor
60       foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
61         enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key
62         static if(key !is null) {
63           auto val = mixin(member);           // get the member's value
64           keyValPairs[key] = toJSON(val);     // add the pair <memberKey> : <memberValue>
65         }
66       }
67       // construct the json object
68       JSONValue json;
69       json.object = keyValPairs;
70       return json;
71     }
72   }
73   else {
74     void populateFromJSON(JSONValue json) {
75       static if (!hasCustomJsonCtor!T) {
76         auto keyValPairs = json.object;
77         // check if each member is actually a member and is marked with the @jsonize attribute
78         foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
79           enum key = jsonizeKey!(this, member);              // find @jsonize, deduce member key
80           static if (key !is null) {
81             alias MemberType = typeof(mixin(member));        // deduce member type
82             auto val = extract!MemberType(keyValPairs[key]); // extract value from json
83             mixin(member ~ "= val;");                        // assign value to member
84           }
85         }
86       }
87     }
88 
89     JSONValue convertToJSON() {
90       JSONValue[string] keyValPairs;
91       // look for members marked with @jsonize, ignore __ctor
92       foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
93         enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key
94         static if(key !is null) {
95           auto val = mixin(member);           // get the member's value
96           keyValPairs[key] = toJSON(val);     // add the pair <memberKey> : <memberValue>
97         }
98       }
99       // construct the json object
100       JSONValue json;
101       json.object = keyValPairs;
102       return json;
103     }
104   }
105 }
106 
107 /// json conversion of primitive types
108 unittest {
109   import std.math : approxEqual;
110   enum Category { one, two }
111 
112   auto j1 = toJSON("bork");
113   assert(j1.type == JSON_TYPE.STRING && j1.str == "bork");
114   assert(extract!string(j1) == "bork");
115 
116   auto j2 = toJSON(4.1);
117   assert(j2.type == JSON_TYPE.FLOAT && j2.floating.approxEqual(4.1));
118   assert(extract!float(j2).approxEqual(4.1));
119   assert(extract!double(j2).approxEqual(4.1));
120   assert(extract!real(j2).approxEqual(4.1));
121 
122   auto j3 = toJSON(41);
123   assert(j3.type == JSON_TYPE.INTEGER && j3.integer == 41);
124   assert(extract!int(j3) == 41);
125   assert(extract!long(j3) == 41);
126 
127   auto j4 = toJSON(41u);
128   assert(j4.type == JSON_TYPE.UINTEGER && j4.uinteger == 41u);
129   assert(extract!uint(j4) == 41u);
130   assert(extract!ulong(j4) == 41u);
131 
132   auto jenum = toJSON!Category(Category.one);
133   assert(jenum.type == JSON_TYPE.STRING);
134   assert(jenum.extract!Category == Category.one);
135 
136   // homogenous json array
137   auto j5 = toJSON([9, 8, 7, 6]);
138   assert(j5.array[0].integer == 9);
139   assert(j5.array[1].integer == 8);
140   assert(j5.array[2].integer == 7);
141   assert(j5.array[3].integer == 6);
142   assert(j5.type == JSON_TYPE.ARRAY);
143   assert(extract!(int[])(j5) == [9, 8, 7, 6]);
144 
145   // heterogenous json array
146   auto j6 = toJSON("sammich", 1.5, 2, 3u);
147   assert(j6.array[0].str == "sammich");
148   assert(j6.array[1].floating.approxEqual(1.5));
149   assert(j6.array[2].integer == 2);
150   assert(j6.array[3].uinteger == 3u);
151 
152   // associative array
153   int[string] aa = ["a" : 1, "b" : 2, "c" : 3];
154   auto j7 = toJSON(aa);
155   assert(j7.type == JSON_TYPE.OBJECT);
156   assert(j7.object["a"].integer == 1);
157   assert(j7.object["b"].integer == 2);
158   assert(j7.object["c"].integer == 3);
159   assert(extract!(int[string])(j7) == aa);
160   assert(j7.extract!int("b") == 2);
161 }
162 
163 /// object serialization -- fields only
164 unittest {
165   import std.math : approxEqual;
166 
167   static class Fields {
168     this() { } // class must have a no-args ctor
169 
170     this(int iVal, float fVal, string sVal, int[] aVal, string noJson) {
171       i = iVal;
172       f = fVal;
173       s = sVal;
174       a = aVal;
175       dontJsonMe = noJson;
176     }
177 
178     mixin JsonizeMe;
179 
180     @jsonize { // fields to jsonize -- test different access levels
181       public int i;
182       protected float f;
183       public int[] a;
184       private string s;
185     }
186     string dontJsonMe;
187 
188     override bool opEquals(Object o) {
189       auto other = cast(Fields) o;
190       return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f);
191     }
192   }
193 
194   auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg");
195   auto json = toJSON!Fields(obj);
196 
197   assert(json.object["i"].integer == 1);
198   assert(json.object["f"].floating.approxEqual(4.2));
199   assert(json.object["s"].str == "tally ho!");
200   assert(json.object["a"].array[0].integer == 9);
201   assert("dontJsonMe" !in json.object);
202 
203   // reconstruct from json
204   auto r = extract!Fields(json);
205   assert(r.i == 1);
206   assert(r.f.approxEqual(4.2));
207   assert(r.s == "tally ho!");
208   assert(r.a == [9, 8, 7, 6]);
209   assert(r.dontJsonMe is null);
210 
211   // array of objects
212   auto a = [
213     new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"),
214         new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus")
215   ];
216 
217   // serialize array of user objects to json
218   auto jsonArray = toJSON!(Fields[])(a);
219   // reconstruct from json
220   assert(extract!(Fields[])(jsonArray) == a);
221 }
222 
223 /// object serialization with properties
224 unittest {
225   import std.math : approxEqual;
226 
227   static class Props {
228     this() { } // class must have a no-args ctor
229 
230     this(int iVal, float fVal, string sVal, string noJson) {
231       _i = iVal;
232       _f = fVal;
233       _s = sVal;
234       _dontJsonMe = noJson;
235     }
236 
237     mixin JsonizeMe;
238 
239     @property {
240       // jsonize ref property accessing private field
241       @jsonize ref int i() { return _i; }
242       // jsonize property with non-trivial get/set methods
243       @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3
244       float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json
245       // don't jsonize these properties
246       ref string s() { return _s; }
247       ref string dontJsonMe() { return _dontJsonMe; }
248     }
249 
250     private:
251     int _i;
252     float _f;
253     @jsonize string _s;
254     string _dontJsonMe;
255   }
256 
257   auto obj = new Props(1, 4.2, "tally ho!", "blarg");
258   auto json = toJSON(obj);
259 
260   assert(json.object["i"].integer == 1);
261   assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval
262   assert(json.object["_s"].str == "tally ho!");
263   assert("dontJsonMe" !in json.object);
264 
265   auto r = extract!Props(json);
266   assert(r.i == 1);
267   assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5
268   assert(r._s == "tally ho!");
269   assert(r.dontJsonMe is null);
270 }
271 
272 /// object serialization with custom constructor
273 unittest {
274   import std.math : approxEqual;
275 
276   static class Custom {
277     mixin JsonizeMe;
278 
279     this(int i) {
280       _i = i;
281       _s = "something";
282       _f = 10.2;
283     }
284 
285     @jsonize this(int _i, string _s, float _f = 20.2) {
286       this._i = _i;
287       this._s = _s ~ " jsonized";
288       this._f = _f;
289     }
290 
291     @jsonize this(double d) { // alternate ctor
292       _f = d.to!float;
293       _s = d.to!string;
294       _i = d.to!int;
295     }
296 
297     private:
298     @jsonize {
299       string _s;
300       float _f;
301       int _i;
302     }
303   }
304 
305   auto c = new Custom(12);
306   auto json = toJSON(c);
307   assert(json.object["_i"].integer == 12);
308   assert(json.object["_s"].str == "something");
309   assert(json.object["_f"].floating.approxEqual(10.2));
310   auto c2 = extract!Custom(json);
311   assert(c2._i == 12);
312   assert(c2._s == "something jsonized");
313   assert(c2._f.approxEqual(10.2));
314 
315   // test alternate ctor
316   json = parseJSON(`{"d" : 5}`);
317   c = json.extract!Custom;
318   assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5");
319 }
320 
321 /// struct serialization
322 unittest {
323   import std.math : approxEqual;
324 
325   static struct S {
326     mixin JsonizeMe;
327 
328     @jsonize {
329       int x;
330       float f;
331       string s;
332     }
333     int dontJsonMe;
334 
335     this(int x, float f, string s, int noJson) {
336       this.x = x;
337       this.f = f;
338       this.s = s;
339       this.dontJsonMe = noJson;
340     }
341   }
342 
343   auto s = S(5, 4.2, "bogus", 7);
344   auto json = toJSON(s); // serialize a struct
345 
346   assert(json.object["x"].integer == 5);
347   assert(json.object["f"].floating.approxEqual(4.2));
348   assert(json.object["s"].str == "bogus");
349   assert("dontJsonMe" !in json.object);
350 
351   auto r = extract!S(json);
352   assert(r.x == 5);
353   assert(r.f.approxEqual(4.2));
354   assert(r.s == "bogus");
355   assert(r.dontJsonMe == int.init);
356 }
357 
358 /// json file I/O
359 unittest {
360   enum file = "test.json";
361   scope(exit) remove(file);
362 
363   static struct Data {
364     mixin JsonizeMe;
365 
366     @jsonize {
367       int x;
368       string s;
369       float f;
370     }
371   }
372 
373   // write an array of user-defined structs
374   auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)];
375   writeJSON(array, file);
376   auto readBack = readJSON!(Data[])(file);
377   assert(readBack == array);
378 
379   // now try an associative array
380   auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)];
381   writeJSON(aa, file);
382   auto aaReadBack = readJSON!(Data[string])(file);
383   assert(aaReadBack == aa);
384 }
385 
386 /// inheritance
387 unittest {
388   import std.math : approxEqual;
389   static class Parent {
390     mixin JsonizeMe;
391     @jsonize {
392       int x;
393       string s;
394     }
395   }
396 
397   static class Child : Parent {
398     mixin JsonizeMe;
399     @jsonize {
400       float f;
401     }
402   }
403 
404   auto c = new Child;
405   c.x = 5;
406   c.s = "hello";
407   c.f = 2.1;
408 
409   auto json = c.toJSON;
410   assert(json.extract!int("x") == 5);
411   assert(json.extract!string("s") == "hello");
412   assert(json.extract!float("f").approxEqual(2.1));
413 
414   auto child = json.extract!Child;
415   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
416 
417   auto parent = json.extract!Parent;
418   assert(parent.x == 5 && parent.s == "hello");
419 }
420 
421 /// inheritance with  ctors
422 unittest {
423   import std.math : approxEqual;
424   static class Parent {
425     mixin JsonizeMe;
426 
427     @jsonize this(int x, string s) {
428       _x = x;
429       _s = s;
430     }
431 
432     @jsonize @property {
433       int x()    { return _x; }
434       string s() { return _s; }
435     }
436 
437     private:
438     int _x;
439     string _s;
440   }
441 
442   static class Child : Parent {
443     mixin JsonizeMe;
444 
445     @jsonize this(int x, string s, float f) {
446       super(x, s);
447       _f = f;
448     }
449 
450     @jsonize @property {
451       float f() { return _f; }
452     }
453 
454     private:
455     float _f;
456   }
457 
458   auto c = new Child(5, "hello", 2.1);
459 
460   auto json = c.toJSON;
461   assert(json.extract!int("x") == 5);
462   assert(json.extract!string("s") == "hello");
463   assert(json.extract!float("f").approxEqual(2.1));
464 
465   auto child = json.extract!Child;
466   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
467 
468   auto parent = json.extract!Parent;
469   assert(parent.x == 5 && parent.s == "hello");
470 }
471 
472 /// renamed members
473 unittest {
474   static class Bleh {
475     mixin JsonizeMe;
476     private {
477       @jsonize("x") int _x;
478       @jsonize("s") string _s;
479     }
480   }
481 
482   auto b = new Bleh;
483   b._x = 5;
484   b._s = "blah";
485 
486   auto json = b.toJSON;
487 
488   assert(json.extract!int("x") == 5);
489   assert(json.extract!string("s") == "blah");
490 
491   auto reconstruct = json.extract!Bleh;
492   assert(reconstruct._x == b._x && reconstruct._s == b._s);
493 }
494 
495 // unfortunately these test classes must be implemented outside the unittest
496 // as Object.factory (and ClassInfo.find) cannot work with nested classes
497 private {
498   class TestComponent {
499     mixin JsonizeMe;
500     @jsonize int c;
501   }
502 
503   class TestCompA : TestComponent {
504     mixin JsonizeMe;
505     @jsonize int a;
506   }
507 
508   class TestCompB : TestComponent {
509     mixin JsonizeMe;
510     @jsonize string b;
511   }
512 }
513 
514 /// type inference
515 unittest {
516   import std.traits : fullyQualifiedName;
517 
518   // need to use these because unittest is assigned weird name
519   // normally would just be "modulename.classname"
520   string classKeyA = fullyQualifiedName!TestCompA;
521   string classKeyB = fullyQualifiedName!TestCompB;
522 
523   assert(Object.factory(classKeyA) !is null && Object.factory(classKeyB) !is null, 
524       "cannot factory classes in unittest -- this is a problem with the test");
525 
526   auto data = `[
527     {
528       "class": "%s",
529       "c": 1,
530       "a": 5
531     },
532     {
533       "class": "%s",
534       "c": 2,
535       "b": "hello"
536     }
537   ]`.format(classKeyA, classKeyB).parseJSON.extract!(TestComponent[]);
538 
539   auto a = cast(TestCompA) data[0];
540   auto b = cast(TestCompB) data[1];
541 
542   assert(a !is null && a.c == 1 && a.a == 5);
543   assert(b !is null && b.c == 2 && b.b == "hello");
544 }