1 /**
2   Utilities releated to JSON processing.
3 */
4 module dcord.util.json;
5 
6 import std.conv,
7        std.meta,
8        std.traits,
9        std.stdio;
10 
11 public import vibe.data.json : VibeJSON = Json, parseJsonString;
12 
13 import dcord.types.base : IModel;
14 public import dcord.util..string : camelCaseToUnderscores;
15 
16 
17 enum JSONIgnore;
18 enum JSONFlat;
19 enum JSONTimestamp;
20 
21 struct JSONSource {
22   string src;
23 }
24 
25 struct JSONListToMap {
26   string field;
27 }
28 
29 VibeJSON serializeToJSON(T)(T sourceObj, string[] ignoredFields = []) {
30   import std.algorithm : canFind;
31 
32   version (JSON_DEBUG_S) {
33     pragma(msg, "Generating Serialization for: ", typeof(sourceObj));
34   }
35 
36   VibeJSON result = VibeJSON.emptyObject;
37   string sourceFieldName, dstFieldName;
38 
39   foreach (fieldName; FieldNameTuple!T) {
40     // Runtime check if we are being ignored
41     if (ignoredFields.canFind(fieldName)) continue;
42 
43     version(JSON_DEBUG_S) {
44       pragma(msg, "  -> ", fieldName);
45     }
46 
47     alias FieldType = typeof(__traits(getMember, sourceObj, fieldName));
48 
49     static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONIgnore)) {
50       version (JSON_DEBUG) {
51         pragma(msg, "    -> skipping");
52         writefln("  -> skipping");
53       }
54       continue;
55     } else static if ((is(FieldType == struct) || is(FieldType == class)) &&
56         hasUDA!(typeof(mixin("sourceObj." ~ fieldName)), JSONIgnore)) {
57       version (JSON_DEBUG) {
58         pragma(msg, "    -> skipping");
59         writefln("  -> skipping");
60       }
61       continue;
62     } else static if (fieldName[0] == '_') {
63       version (JSON_DEBUG) {
64         pragma(msg, "    -> skipping");
65         writefln("  -> skipping");
66       }
67       continue;
68     } else {
69         static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONSource)) {
70           dstFieldName = getUDAs!(mixin("sourceObj." ~ fieldName), JSONSource)[0].src;
71         } else {
72           dstFieldName = camelCaseToUnderscores(fieldName);
73         }
74 
75 
76       static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONListToMap)) {
77         version (JSON_DEBUG) pragma(msg, "    -= TODO");
78         // TODO
79         /+
80           __traits(getMember, sourceObj, fieldName) = typeof(__traits(getMember, sourceObj, fieldName)).fromJSONArray!(
81             getUDAs!(mixin("sourceObj." ~ fieldName), JSONListToMap)[0].field
82           )(sourceObj, fieldData);
83         +/
84       } else {
85         version (JSON_DEBUG) pragma(msg, "    -= dumpSingleField");
86         result[dstFieldName] = dumpSingleField(mixin("sourceObj." ~ fieldName));
87       }
88     }
89   }
90 
91   return result;
92 }
93 
94 private VibeJSON dumpSingleField(T)(ref T field) {
95   static if (is(T == struct)) {
96     return field.serializeToJSON;
97   } else static if (is(T == class)) {
98     return field ? field.serializeToJSON : VibeJSON(null);
99   } else static if (isSomeString!T) {
100     return VibeJSON(field);
101   } else static if (isArray!T) {
102     return VibeJSON();
103     // TODO
104   } else {
105     return VibeJSON(field);
106   }
107 }
108 
109 void deserializeFromJSON(T)(T sourceObj, VibeJSON sourceData) {
110   version (JSON_DEBUG) {
111     pragma(msg, "Generating Deserialization for: ", typeof(sourceObj));
112   }
113 
114   string sourceFieldName, dstFieldName;
115   VibeJSON fieldData;
116 
117   foreach (fieldName; FieldNameTuple!T) {
118     version (JSON_DEBUG) {
119       pragma(msg, "  -> ", fieldName);
120       writefln("%s", fieldName);
121     }
122 
123     alias FieldType = typeof(__traits(getMember, sourceObj, fieldName));
124 
125     // First we need to check whether we should ignore this field
126     static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONIgnore)) {
127       version (JSON_DEBUG) {
128         pragma(msg, "    -> skipping");
129         writefln("  -> skipping");
130       }
131       continue;
132     } else static if ((is(FieldType == struct) || is(FieldType == class)) &&
133         hasUDA!(typeof(mixin("sourceObj." ~ fieldName)), JSONIgnore)) {
134       version (JSON_DEBUG) {
135         pragma(msg, "    -> skipping");
136         writefln("  -> skipping");
137       }
138       continue;
139     } else static if (fieldName[0] == '_') {
140       version (JSON_DEBUG) {
141         pragma(msg, "    -> skipping");
142         writefln("  -> skipping");
143       }
144       continue;
145     } else {
146       // Now we grab the data
147       static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONFlat)) {
148         fieldData = sourceData;
149       } else {
150         static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONSource)) {
151           sourceFieldName = getUDAs!(mixin("sourceObj." ~ fieldName), JSONSource)[0].src;
152         } else {
153           sourceFieldName = camelCaseToUnderscores(fieldName);
154         }
155 
156         if (
157             (sourceFieldName !in sourceData) ||
158             (sourceData[sourceFieldName].type == VibeJSON.Type.undefined) ||
159             (sourceData[sourceFieldName].type == VibeJSON.Type.null_)) {
160           continue;
161         }
162 
163         fieldData = sourceData[sourceFieldName];
164       }
165 
166       // Now we parse the data
167       version (JSON_DEBUG) {
168         writefln("  -> src from %s", fieldData);
169       }
170 
171       // meh
172       static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONListToMap)) {
173         version (JSON_DEBUG) pragma(msg, "    -= JSONListToMap");
174         __traits(getMember, sourceObj, fieldName) = typeof(__traits(getMember, sourceObj, fieldName)).fromJSONArray!(
175           getUDAs!(mixin("sourceObj." ~ fieldName), JSONListToMap)[0].field
176         )(sourceObj, fieldData);
177       } else static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONTimestamp)) {
178         version (JSON_DEBUG) pragma(msg, "    -= loadTimestampField");
179         __traits(getMember, sourceObj, fieldName) = loadTimestampField!(T, FieldType)(sourceObj, fieldData);
180       } else {
181         version (JSON_DEBUG) pragma(msg, "    -= loadSingleField");
182         __traits(getMember, sourceObj, fieldName) = loadSingleField!(T, FieldType)(sourceObj, fieldData);
183       }
184     }
185   }
186 }
187 
188 template ArrayElementType(T : T[]) {
189   alias T ArrayElementType;
190 }
191 
192 template AATypes(T) {
193   alias ArrayElementType!(typeof(T.keys)) key;
194   alias ArrayElementType!(typeof(T.values)) value;
195 }
196 
197 private DateT loadTimestampField(T, DateT)(T sourceObj, VibeJSON data) {
198   return DateT.fromISOExtString(data.to!string);
199 }
200 
201 private Z loadSingleField(T, Z)(T sourceObj, VibeJSON data) {
202   version (JSON_DEBUG) {
203     writefln("  -> parsing type %s from %s", fullyQualifiedName!Z, data.type);
204   }
205 
206   // Some deserialization strategies we take require a reference to the type.
207   Z result;
208 
209   static if (is(Z == VibeJSON)) {
210     return data;
211   } else static if (is(Z == struct)) {
212     result.deserializeFromJSON(data);
213     return result;
214   } else static if (is(Z == enum)) {
215     // Read the stored type and then cast it to our enum type
216     return cast(Z)data.to!(OriginalType!Z);
217   } else static if (is(Z == class)) {
218     // If we have a constructor which allows the parent object and the JSON data use it
219     static if (__traits(compiles, {
220       new Z(sourceObj, data);
221     })) {
222       result = new Z(sourceObj, data);
223       result.attach(sourceObj);
224     } else static if (hasMember!(Z, "client")) {
225       result = new Z(__traits(getMember, sourceObj, "client"), data);
226       result.attach(sourceObj);
227     } else {
228       result = new Z;
229       result.deserializeFromJSON(data);
230     }
231     return result;
232   } else static if (isSomeString!Z) {
233     static if (__traits(compiles, {
234       return cast(Z)data.get!string;
235     })) {
236       return cast(Z)data.get!string;
237     } else {
238       return data.get!string.to!Z;
239     }
240   } else static if (isArray!Z) {
241     alias AT = ArrayElementType!(Z);
242 
243     foreach (obj; data) {
244       AT v = loadSingleField!(T, AT)(sourceObj, obj);
245       result ~= v;
246     }
247     return result;
248   } else static if (isAssociativeArray!Z) {
249     alias ArrayElementType!(typeof(result.keys)) Tk;
250     alias ArrayElementType!(typeof(result.values)) Tv;
251 
252     foreach (ref string k, ref v; data) {
253       Tv val = loadSingleField!(T, Tv)(sourceObj, v);
254 
255       result[k.to!Tk] = val;
256     }
257     return result;
258   } else static if (isIntegral!Z) {
259     if (data.type == VibeJSON.Type..string) {
260       return data.get!string.to!Z;
261     } else {
262       static if (__traits(compiles, { result = data.to!Z; })) {
263         return data.to!Z;
264       } else {
265         return data.get!Z;
266       }
267     }
268   } else {
269     return data.to!Z;
270   }
271 }
272 
273 private void attach(T, Z)(T baseObj, Z parentObj) {
274   foreach (fieldName; FieldNameTuple!T) {
275     alias FieldType = typeof(__traits(getMember, baseObj, fieldName));
276 
277     static if (is(FieldType == Z)) {
278       __traits(getMember, baseObj, fieldName) = parentObj;
279     }
280   }
281 }
282 
283 T deserializeFromJSON(T)(VibeJSON jsonData) {
284   T result = new T;
285   result.deserializeFromJSON(jsonData);
286   return result;
287 }
288 
289 T[] deserializeFromJSONArray(T)(VibeJSON jsonData, T delegate(VibeJSON) cons) {
290   T[] result;
291 
292   foreach (item; jsonData) {
293     result ~= cons(item);
294   }
295 
296   return result;
297 }
298 
299 unittest {
300   // Test Enums
301   enum TestEnum {
302     A = "A",
303     B = "B",
304   }
305 
306   class TestEnumClass {
307     TestEnum test;
308   }
309 
310   (new TestEnumClass()).deserializeFromJSON(
311     parseJsonString(q{{"test": "A"}}),
312   );
313 
314   // Test Timestamp
315   import std.datetime : SysTime;
316 
317   class TestTimestampClass {
318     @JSONTimestamp
319     SysTime timestamp;
320   }
321 
322   (new TestTimestampClass()).deserializeFromJSON(
323     parseJsonString(q{{"timestamp": "2018-11-13T02:51:57.736000+00:00"}}),
324   );
325 }