1 /// `extract!T` converts a `JSONValue` to an instance of `T`
2 module internal.extract;
3 
4 import std.json;
5 import std.conv;
6 import std.range;
7 import std.traits;
8 import std.string;
9 import std.algorithm;
10 import std.exception;
11 import std.typetuple;
12 import std.typecons : staticIota;
13 import internal.attribute;
14 
15 /// json member used to map a json object to a D type  
16 enum jsonizeClassKeyword = "class";
17 
18 private void enforceJsonType(T)(JSONValue json, JSON_TYPE[] expected ...) {
19   enum fmt = "extract!%s expected json type to be one of %s but got json type %s. json input: %s";
20   enforce(expected.canFind(json.type), format(fmt, typeid(T), expected, json.type, json));
21 }
22 
23 /// extract a boolean from a json value
24 T extract(T : bool)(JSONValue json) {
25   if (json.type == JSON_TYPE.TRUE) {
26     return true;
27   }
28   else if (json.type == JSON_TYPE.FALSE) {
29     return false;
30   }
31   else {
32     enforce(0, format("tried to extract bool from json of type %s", json.type));
33   }
34 }
35 
36 /// extract a string type from a json value
37 T extract(T : string)(JSONValue json) {
38   if (json.type == JSON_TYPE.NULL) { return null; }
39   enforceJsonType!T(json, JSON_TYPE.STRING);
40   return cast(T) json.str;
41 }
42 
43 /// extract a numeric type from a json value
44 T extract(T : real)(JSONValue json) if (!is(T == enum)) {
45   switch(json.type) {
46     case JSON_TYPE.FLOAT:
47       return cast(T) json.floating;
48     case JSON_TYPE.INTEGER:
49       return cast(T) json.integer;
50     case JSON_TYPE.UINTEGER:
51       return cast(T) json.uinteger;
52     case JSON_TYPE.STRING:
53       enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str));
54       return to!T(json.str); // try to parse string as int
55     default:
56       enforce(0, format("tried to extract %s from json of type %s", T.stringof, json.type));
57   }
58   assert(0, "should not be reacheable");
59 }
60 
61 /// extract an enumerated type from a json value
62 T extract(T)(JSONValue json) if (is(T == enum)) {
63   enforceJsonType!T(json, JSON_TYPE.STRING);
64   return to!T(json.str);
65 }
66 
67 /// extract an array from a JSONValue
68 T extract(T)(JSONValue json) if (isArray!T && !isSomeString!(T)) {
69   if (json.type == JSON_TYPE.NULL) { return T.init; }
70   enforceJsonType!T(json, JSON_TYPE.ARRAY);
71   alias ElementType = ForeachType!T;
72   T vals;
73   foreach(idx, val ; json.array) {
74     static if (isStaticArray!T) {
75       vals[idx] = val.extract!ElementType;
76     }
77     else {
78       vals ~= val.extract!ElementType;
79     }
80   }
81   return vals;
82 }
83 
84 /// extract an associative array from a JSONValue
85 T extract(T)(JSONValue json) if (isAssociativeArray!T) {
86   assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
87   if (json.type == JSON_TYPE.NULL) { return null; }
88   enforceJsonType!T(json, JSON_TYPE.OBJECT);
89   alias ValType = ValueType!T;
90   T map;
91   foreach(key, val ; json.object) {
92     map[key] = extract!ValType(val);
93   }
94   return map;
95 }
96 
97 /// extract a value from a json object by its key
98 T extract(T)(JSONValue json, string key) {
99   enforceJsonType!T(json, JSON_TYPE.OBJECT);
100   enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue");
101   return extract!T(json.object[key]);
102 }
103 
104 /// extract a value from a json object by its key, return defaultVal if key not found
105 T extract(T)(JSONValue json, string key, T defaultVal) {
106   enforceJsonType!T(json, JSON_TYPE.OBJECT);
107   return (key in json.object) ? extract!T(json.object[key]) : defaultVal;
108 }
109 
110 /// extract a user-defined class or struct from a JSONValue
111 T extract(T)(JSONValue json) if (!isBuiltinType!T) {
112   static if (is(T == class)) {
113     if (json.type == JSON_TYPE.NULL) { return null; }
114   }
115   enforceJsonType!T(json, JSON_TYPE.OBJECT);
116 
117   static if (is(typeof(null) : T)) {
118     // look for class keyword in json
119     auto className = json.extract!string(jsonizeClassKeyword, null);
120     // try creating an instance with Object.factory 
121     if (className !is null) {
122       auto obj = Object.factory(className);
123       assert(obj !is null, "failed to Object.factory " ~ className);
124       auto instance = cast(T) obj;
125       assert(instance !is null, "failed to cast " ~ className ~ " to " ~ T.stringof);
126       instance.populateFromJSON(json);
127       return instance;
128     }
129   }
130 
131   // next, try to find a contructor marked with @jsonize and call that
132   static if (__traits(hasMember, T, "__ctor")) {
133     alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor"));
134     foreach(overload ; Overloads) {
135       if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0 &&
136           canSatisfyCtor!overload(json)) {
137         return invokeCustomJsonCtor!(T, overload)(json);
138       }
139     }
140   }
141 
142   // if no @jsonized ctor, try to use a default ctor and populate the fields
143   static if(is(T == struct) || is(typeof(new T) == T)) { // can object be default constructed?
144     return invokeDefaultCtor!(T)(json);
145   }
146   assert(0, T.stringof ~ " must have a no-args constructor to support extract");
147 }
148 
149 /// return true if keys can satisfy parameter names
150 private bool canSatisfyCtor(alias Ctor)(JSONValue json) {
151   auto obj = json.object;
152   alias Params   = ParameterIdentifierTuple!Ctor;
153   alias Types    = ParameterTypeTuple!Ctor;
154   alias Defaults = ParameterDefaultValueTuple!Ctor;
155   foreach(i ; staticIota!(0, Params.length)) {
156     if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) {
157       return false; // param had no default value and was not specified
158     }
159   }
160   return true;
161 }
162 
163 private T invokeCustomJsonCtor(T, alias Ctor)(JSONValue json) {
164   enum params    = ParameterIdentifierTuple!(Ctor);
165   alias defaults = ParameterDefaultValueTuple!(Ctor);
166   alias Types    = ParameterTypeTuple!(Ctor);
167   Tuple!(Types) args;
168   foreach(i ; staticIota!(0, params.length)) {
169     enum paramName = params[i];
170     if (paramName in json.object) {
171       args[i] = json.object[paramName].extract!(Types[i]);
172     }
173     else { // no value specified in json
174       static if (is(defaults[i] == void)) {
175         enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified");
176       }
177       else {
178         args[i] = defaults[i];
179       }
180     }
181   }
182   static if (is(T == class)) {
183     return new T(args.expand);
184   }
185   else {
186     return T(args.expand);
187   }
188 }
189 
190 private T invokeDefaultCtor(T)(JSONValue json) {
191   T obj;
192   static if (is(T == struct)) {
193     obj = T.init;
194   }
195   else {
196     obj = new T;
197   }
198   obj.populateFromJSON(json);
199   return obj;
200 }
201 
202 bool hasCustomJsonCtor(T)() {
203   static if (__traits(hasMember, T, "__ctor")) {
204     alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor"));
205     foreach(overload ; Overloads) {
206       static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) {
207         return true;
208       }
209     }
210   }
211   return false;
212 }