1 /**
2   * Contains functions for deserializing JSON data.
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.fromjson;
10 
11 import std.json;
12 import std.conv;
13 import std.file;
14 import std.range;
15 import std.traits;
16 import std.string;
17 import std.algorithm;
18 import std.exception;
19 import std.typetuple;
20 import std.typecons : staticIota, Tuple;
21 import jsonizer.exceptions : JsonizeTypeException, JsonizeConstructorException;
22 import jsonizer.internal.attribute;
23 import jsonizer.internal.util;
24 
25 /// json member used to map a json object to a D type
26 enum jsonizeClassKeyword = "class";
27 
28 private void enforceJsonType(T)(JSONValue json, JSON_TYPE[] expected ...) {
29   if (!expected.canFind(json.type)) {
30     throw new JsonizeTypeException(typeid(T), json, expected);
31   }
32 }
33 
34 unittest {
35   import std.exception : assertThrown, assertNotThrown;
36   with (JSON_TYPE) {
37     assertThrown!JsonizeTypeException(enforceJsonType!int(JSONValue("hi"), INTEGER, UINTEGER));
38     assertThrown!JsonizeTypeException(enforceJsonType!(bool[string])(JSONValue([ 5 ]), OBJECT));
39     assertNotThrown(enforceJsonType!int(JSONValue(5), INTEGER, UINTEGER));
40     assertNotThrown(enforceJsonType!(bool[string])(JSONValue(["key": true]), OBJECT));
41   }
42 }
43 
44 deprecated("use fromJSON instead") {
45   /// Deprecated: use `fromJSON` instead.
46   T extract(T)(JSONValue json) {
47     return json.fromJSON!T;
48   }
49 }
50 
51 /// Extract a boolean from a json value.
52 T fromJSON(T : bool)(JSONValue json) {
53   if (json.type == JSON_TYPE.TRUE) {
54     return true;
55   }
56   else if (json.type == JSON_TYPE.FALSE) {
57     return false;
58   }
59 
60   // expected 'true' or 'false'
61   throw new JsonizeTypeException(typeid(bool), json, JSON_TYPE.TRUE, JSON_TYPE.FALSE);
62 }
63 
64 /// Extract booleans from json values.
65 unittest {
66   assert(JSONValue(false).fromJSON!bool == false);
67   assert(JSONValue(true).fromJSON!bool == true);
68 }
69 
70 /// Extract a string type from a json value.
71 T fromJSON(T : string)(JSONValue json) {
72   if (json.type == JSON_TYPE.NULL) { return null; }
73   enforceJsonType!T(json, JSON_TYPE.STRING);
74   return cast(T) json.str;
75 }
76 
77 /// Extract a string from a json string.
78 unittest {
79   assert(JSONValue("asdf").fromJSON!string == "asdf");
80 }
81 
82 /// Extract a numeric type from a json value.
83 T fromJSON(T : real)(JSONValue json) if (!is(T == enum)) {
84   switch(json.type) {
85     case JSON_TYPE.FLOAT:
86       return cast(T) json.floating;
87     case JSON_TYPE.INTEGER:
88       return cast(T) json.integer;
89     case JSON_TYPE.UINTEGER:
90       return cast(T) json.uinteger;
91     case JSON_TYPE.STRING:
92       enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str));
93       return to!T(json.str); // try to parse string as int
94     default:
95   }
96 
97   throw new JsonizeTypeException(typeid(bool), json,
98       JSON_TYPE.FLOAT, JSON_TYPE.INTEGER, JSON_TYPE.UINTEGER, JSON_TYPE.STRING);
99 }
100 
101 /// Extract various numeric types.
102 unittest {
103   assert(JSONValue(1).fromJSON!int      == 1);
104   assert(JSONValue(2u).fromJSON!uint    == 2u);
105   assert(JSONValue(3.0).fromJSON!double == 3.0);
106 
107   // fromJSON accepts numeric strings when a numeric conversion is requested
108   assert(JSONValue("4").fromJSON!long   == 4L);
109 }
110 
111 /// Extract an enumerated type from a json value.
112 T fromJSON(T)(JSONValue json) if (is(T == enum)) {
113   enforceJsonType!T(json, JSON_TYPE.STRING);
114   return to!T(json.str);
115 }
116 
117 /// Convert a json string into an enum value.
118 unittest {
119   enum Category { one, two }
120   assert(JSONValue("one").fromJSON!Category == Category.one);
121 }
122 
123 /// Extract an array from a JSONValue.
124 T fromJSON(T)(JSONValue json) if (isArray!T && !isSomeString!(T)) {
125   if (json.type == JSON_TYPE.NULL) { return T.init; }
126   enforceJsonType!T(json, JSON_TYPE.ARRAY);
127   alias ElementType = ForeachType!T;
128   T vals;
129   foreach(idx, val ; json.array) {
130     static if (isStaticArray!T) {
131       vals[idx] = val.fromJSON!ElementType;
132     }
133     else {
134       vals ~= val.fromJSON!ElementType;
135     }
136   }
137   return vals;
138 }
139 
140 /// Convert a json array into an array.
141 unittest {
142   auto a = [ 1, 2, 3 ];
143   assert(JSONValue(a).fromJSON!(int[]) == a);
144 }
145 
146 /// Extract an associative array from a JSONValue.
147 T fromJSON(T)(JSONValue json) if (isAssociativeArray!T) {
148   static assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
149   if (json.type == JSON_TYPE.NULL) { return null; }
150   enforceJsonType!T(json, JSON_TYPE.OBJECT);
151   alias ValType = ValueType!T;
152   T map;
153   foreach(key, val ; json.object) {
154     map[key] = fromJSON!ValType(val);
155   }
156   return map;
157 }
158 
159 /// Convert a json object to an associative array.
160 unittest {
161   auto aa = ["a": 1, "b": 2];
162   assert(JSONValue(aa).fromJSON!(int[string]) == aa);
163 }
164 
165 /// Extract a value from a json object by its key.
166 T fromJSON(T)(JSONValue json, string key) {
167   enforceJsonType!T(json, JSON_TYPE.OBJECT);
168   enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue");
169   return fromJSON!T(json.object[key]);
170 }
171 
172 unittest {
173   auto aa = ["a": 1, "b": 2];
174   auto json = JSONValue(aa);
175   assert(json.fromJSON!int("a") == 1);
176   assert(json.fromJSON!ulong("b") == 2L);
177 }
178 
179 /// Extract a value from a json object by its key, return `defaultVal` if key not found.
180 T fromJSON(T)(JSONValue json, string key, T defaultVal) {
181   enforceJsonType!T(json, JSON_TYPE.OBJECT);
182   return (key in json.object) ? fromJSON!T(json.object[key]) : defaultVal;
183 }
184 
185 /// Substitute default values when keys aren't present.
186 unittest {
187   auto aa = ["a": 1, "b": 2];
188   auto json = JSONValue(aa);
189   assert(json.fromJSON!int("a", 7) == 1);
190   assert(json.fromJSON!int("c", 7) == 7);
191 }
192 
193 /// Convert a json value in its string representation into a type `T`
194 /// Params:
195 ///    T    = target type
196 ///    json = json string to deserialize
197 T fromJSONString(T)(string json) {
198   return fromJSON!T(json.parseJSON);
199 }
200 
201 unittest {
202   assert(fromJSONString!(int[])("[1, 2, 3]") == [1, 2, 3]);
203 }
204 
205 /// Read a json-constructable object from a file.
206 /// Params:
207 ///   path = filesystem path to json file
208 /// Returns: object parsed from json file
209 T readJSON(T)(string path) {
210   auto json = parseJSON(readText(path));
211   return fromJSON!T(json);
212 }
213 
214 /// Read a json file directly into a specified D type.
215 unittest {
216   import std.path : buildPath;
217   import std.uuid : randomUUID;
218   import std.file : tempDir, write, mkdirRecurse;
219 
220   auto dir = buildPath(tempDir(), "jsonizer_readjson_test");
221   mkdirRecurse(dir);
222   auto file = buildPath(dir, randomUUID().toString);
223 
224   file.write("[1, 2, 3]");
225 
226   assert(file.readJSON!(int[]) == [ 1, 2, 3 ]);
227 }
228 
229 /// Read contents of a json file directly into a JSONValue.
230 /// Params:
231 ///   path = filesystem path to json file
232 /// Returns: a `JSONValue` parsed from the file
233 auto readJSON(string path) {
234   return parseJSON(readText(path));
235 }
236 
237 /// Read a json file into a JSONValue.
238 unittest {
239   import std.path : buildPath;
240   import std.uuid : randomUUID;
241   import std.file : tempDir, write, mkdirRecurse;
242 
243   auto dir = buildPath(tempDir(), "jsonizer_readjson_test");
244   mkdirRecurse(dir);
245   auto file = buildPath(dir, randomUUID().toString);
246 
247   file.write("[1, 2, 3]");
248 
249   auto json = file.readJSON();
250 
251   assert(json.array[0].integer == 1);
252   assert(json.array[1].integer == 2);
253   assert(json.array[2].integer == 3);
254 }
255 
256 /// Extract a user-defined class or struct from a JSONValue.
257 /// See `jsonizer.jsonize` for info on how to mark your own types for serialization.
258 T fromJSON(T)(JSONValue json) if (!isBuiltinType!T) {
259   return fromJSONImpl!T(json, null);
260 }
261 
262 Inner nestedFromJSON(Inner, Outer)(JSONValue json, Outer outer) {
263   return fromJSONImpl!Inner(json, outer);
264 }
265 
266 // Internal implementation of fromJSON for user-defined types
267 // If T is a nested class, pass the parent of type P
268 // otherwise pass null for the parent
269 T fromJSONImpl(T, P)(JSONValue json, P parent = null) if (!isBuiltinType!T) {
270   static if (is(typeof(null) : T)) {
271     if (json.type == JSON_TYPE.NULL) { return null; }
272   }
273 
274   // try constructing from a primitive type using a single-param constructor
275   if (json.type != JSON_TYPE.OBJECT) {
276     return invokePrimitiveCtor!T(json, parent);
277   }
278 
279   static if (!isNested!T && is(T == class) && is(typeof(T.init.populateFromJSON)))
280   {
281     // look for class keyword in json
282     auto className = json.fromJSON!string(jsonizeClassKeyword, null);
283     // try creating an instance with Object.factory
284     if (className !is null) {
285       auto obj = Object.factory(className);
286       assert(obj !is null, "failed to Object.factory " ~ className);
287       auto instance = cast(T) obj;
288       assert(instance !is null, "failed to cast " ~ className ~ " to " ~ T.stringof);
289       instance.populateFromJSON(json);
290       return instance;
291     }
292   }
293 
294   // next, try to find a contructor marked with @jsonize and call that
295   static if (__traits(hasMember, T, "__ctor")) {
296     alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor"));
297     foreach(overload ; Overloads) {
298       static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) {
299         if (canSatisfyCtor!overload(json)) {
300           return invokeCustomJsonCtor!(T, overload)(json, parent);
301         }
302       }
303     }
304 
305     // no constructor worked, is default-construction an option?
306     static if(!hasDefaultCtor!T) {
307       // not default-constructable, need to fail here
308       alias ctors = Filter!(isJsonized, __traits(getOverloads, T, "__ctor"));
309       JsonizeConstructorException.doThrow!(T, ctors)(json);
310     }
311   }
312 
313   // if no @jsonized ctor, try to use a default ctor and populate the fields
314   static if(hasDefaultCtor!T) {
315     return invokeDefaultCtor!(T)(json, parent);
316   }
317 
318   assert(0, "all attempts at deserializing " ~ fullyQualifiedName!T ~ " failed.");
319 }
320 
321 /// Deserialize an instance of a user-defined type from a json object.
322 unittest {
323   import jsonizer.jsonize;
324   import jsonizer.tojson;
325 
326   static struct Foo {
327     mixin JsonizeMe;
328 
329     @jsonize {
330       int i;
331       string[] a;
332     }
333   }
334 
335   auto jstr = q{
336     {
337       "i": 1,
338       "a": [ "a", "b" ]
339     }
340   };
341 
342   // you could use `readJSON` instead of `parseJSON.fromJSON`
343   auto foo = jstr.parseJSON.fromJSON!Foo;
344   assert(foo.i == 1);
345   assert(foo.a == [ "a", "b" ]);
346 }
347 
348 // return true if keys can satisfy parameter names
349 private bool canSatisfyCtor(alias Ctor)(JSONValue json) {
350   auto obj = json.object;
351   alias Params   = ParameterIdentifierTuple!Ctor;
352   alias Types    = ParameterTypeTuple!Ctor;
353   alias Defaults = ParameterDefaultValueTuple!Ctor;
354   foreach(i ; staticIota!(0, Params.length)) {
355     if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) {
356       return false; // param had no default value and was not specified
357     }
358   }
359   return true;
360 }
361 
362 private T invokeCustomJsonCtor(T, alias Ctor, P)(JSONValue json, P parent) {
363   enum params    = ParameterIdentifierTuple!(Ctor);
364   alias defaults = ParameterDefaultValueTuple!(Ctor);
365   alias Types    = ParameterTypeTuple!(Ctor);
366   Tuple!(Types) args;
367   foreach(i ; staticIota!(0, params.length)) {
368     enum paramName = params[i];
369     if (paramName in json.object) {
370       args[i] = json.object[paramName].fromJSON!(Types[i]);
371     }
372     else { // no value specified in json
373       static if (is(defaults[i] == void)) {
374         enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified");
375       }
376       else {
377         args[i] = defaults[i];
378       }
379     }
380   }
381   static if (isNested!T) {
382     return constructNested!T(parent, args.expand);
383   }
384   else static if (is(T == class)) {
385     return new T(args.expand);
386   }
387   else {
388     return T(args.expand);
389   }
390 }
391 
392 private Inner constructNested(Inner, Outer, Args ...)(Outer outer, Args args) {
393   return outer..new Inner(args);
394 }
395 
396 private T invokeDefaultCtor(T, P)(JSONValue json, P parent) {
397   T obj;
398 
399   static if (isNested!T) {
400     obj = parent..new T;
401   }
402   else static if (is(T == struct)) {
403     obj = T.init;
404   }
405   else {
406     obj = new T;
407   }
408 
409   obj.populateFromJSON(json);
410   return obj;
411 }
412 
413 private template isJsonized(alias member) {
414   static bool helper() {
415     foreach(attr ; __traits(getAttributes, member)) {
416       static if (is(attr == jsonize) || is(typeof(attr) == jsonize)) {
417         return true;
418       }
419     }
420     return false;
421   }
422 
423   enum isJsonized = helper();
424 }
425 
426 private T invokePrimitiveCtor(T, P)(JSONValue json, P parent) {
427   static if (__traits(hasMember, T, "__ctor")) {
428     foreach(overload ; __traits(getOverloads, T, "__ctor")) {
429       alias Types = ParameterTypeTuple!overload;
430 
431       // look for an @jsonized ctor with a single parameter
432       static if (hasAttribute!(jsonize, overload) && Types.length == 1) {
433         return construct!T(parent, json.fromJSON!(Types[0]));
434       }
435     }
436   }
437 
438   throw new JsonizeTypeException(typeid(T), json, JSON_TYPE.OBJECT);
439 }
440 
441 unittest {
442   static class Foo {
443     @jsonize int i;
444     @jsonize("s") string _s;
445     @jsonize @property string sprop() { return _s; }
446     @jsonize @property void sprop(string str) { _s = str; }
447     float f;
448     @property int iprop() { return i; }
449 
450     @jsonize this(string s) { }
451     this(int i) { }
452   }
453 
454   Foo f;
455   static assert(isJsonized!(__traits(getMember, f, "i")));
456   static assert(isJsonized!(__traits(getMember, f, "_s")));
457   static assert(isJsonized!(__traits(getMember, f, "sprop")));
458   static assert(isJsonized!(__traits(getMember, f, "i")));
459   static assert(!isJsonized!(__traits(getMember, f, "f")));
460   static assert(!isJsonized!(__traits(getMember, f, "iprop")));
461 
462   import std.typetuple : Filter;
463   static assert(Filter!(isJsonized, __traits(getOverloads, Foo, "__ctor")).length == 1);
464 }