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       } else {
79         version (JSON_DEBUG) pragma(msg, "    -= dumpSingleField");
80         result[dstFieldName] = dumpSingleField(mixin("sourceObj." ~ fieldName));
81       }
82     }
83   }
84 
85   return result;
86 }
87 
88 private VibeJSON dumpSingleField(T)(ref T field) {
89   static if (is(T == struct)) {
90     return field.serializeToJSON;
91   } else static if (is(T == class)) {
92     return field ? field.serializeToJSON : VibeJSON(null);
93   } else static if (isSomeString!T) {
94     return VibeJSON(field);
95   } else static if (isArray!T) {
96     return VibeJSON();
97     // TODO
98   } else {
99     return VibeJSON(field);
100   }
101 }
102 
103 void deserializeFromJSON(T)(T sourceObj, VibeJSON sourceData) {
104   version (JSON_DEBUG) {
105     pragma(msg, "Generating Deserialization for: ", typeof(sourceObj));
106   }
107 
108   string sourceFieldName, dstFieldName;
109   VibeJSON fieldData;
110 
111   foreach (fieldName; FieldNameTuple!T) {
112     version (JSON_DEBUG) {
113       pragma(msg, "  -> ", fieldName);
114       writefln("%s", fieldName);
115     }
116 
117     alias FieldType = typeof(__traits(getMember, sourceObj, fieldName));
118 
119     // First we need to check whether we should ignore this field
120     static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONIgnore)) {
121       version (JSON_DEBUG) {
122         pragma(msg, "    -> skipping");
123         writefln("  -> skipping");
124       }
125       continue;
126     } else static if ((is(FieldType == struct) || is(FieldType == class)) &&
127         hasUDA!(typeof(mixin("sourceObj." ~ fieldName)), JSONIgnore)) {
128       version (JSON_DEBUG) {
129         pragma(msg, "    -> skipping");
130         writefln("  -> skipping");
131       }
132       continue;
133     } else static if (fieldName[0] == '_') {
134       version (JSON_DEBUG) {
135         pragma(msg, "    -> skipping");
136         writefln("  -> skipping");
137       }
138       continue;
139     } else {
140       // Now we grab the data
141       static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONFlat)) {
142         fieldData = sourceData;
143       } else {
144         static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONSource)) {
145           sourceFieldName = getUDAs!(mixin("sourceObj." ~ fieldName), JSONSource)[0].src;
146         } else {
147           sourceFieldName = camelCaseToUnderscores(fieldName);
148         }
149 
150         if (
151             (sourceFieldName !in sourceData) ||
152             (sourceData[sourceFieldName].type == VibeJSON.Type.undefined) ||
153             (sourceData[sourceFieldName].type == VibeJSON.Type.null_)) {
154           continue;
155         }
156 
157         fieldData = sourceData[sourceFieldName];
158       }
159 
160       // Now we parse the data
161       version (JSON_DEBUG) {
162         writefln("  -> src from %s", fieldData);
163       }
164 
165       // meh
166       static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONListToMap)) {
167         version (JSON_DEBUG) pragma(msg, "    -= JSONListToMap");
168         __traits(getMember, sourceObj, fieldName) = typeof(__traits(getMember, sourceObj, fieldName)).fromJSONArray!(
169           getUDAs!(mixin("sourceObj." ~ fieldName), JSONListToMap)[0].field
170         )(sourceObj, fieldData);
171       } else static if (hasUDA!(mixin("sourceObj." ~ fieldName), JSONTimestamp)) {
172         version (JSON_DEBUG) pragma(msg, "    -= loadTimestampField");
173         __traits(getMember, sourceObj, fieldName) = loadTimestampField!(T, FieldType)(sourceObj, fieldData);
174       } else {
175         version (JSON_DEBUG) pragma(msg, "    -= loadSingleField");
176         __traits(getMember, sourceObj, fieldName) = loadSingleField!(T, FieldType)(sourceObj, fieldData);
177       }
178     }
179   }
180 }
181 
182 template ArrayElementType(T : T[]) {
183   alias T ArrayElementType;
184 }
185 
186 template AATypes(T) {
187   alias ArrayElementType!(typeof(T.keys)) key;
188   alias ArrayElementType!(typeof(T.values)) value;
189 }
190 
191 private DateT loadTimestampField(T, DateT)(T sourceObj, VibeJSON data) {
192   return DateT.fromISOExtString(data.to!string);
193 }
194 
195 private Z loadSingleField(T, Z)(T sourceObj, VibeJSON data) {
196   version (JSON_DEBUG) {
197     writefln("  -> parsing type %s from %s", fullyQualifiedName!Z, data.type);
198   }
199 
200   // Some deserialization strategies we take require a reference to the type.
201   Z result;
202 
203   static if (is(Z == VibeJSON)) {
204     return data;
205   } else static if (is(Z == struct)) {
206     result.deserializeFromJSON(data);
207     return result;
208   } else static if (is(Z == enum)) {
209     // Read the stored type and then cast it to our enum type
210     return cast(Z)data.to!(OriginalType!Z);
211   } else static if (is(Z == class)) {
212     // If we have a constructor which allows the parent object and the JSON data use it
213     static if (__traits(compiles, {
214       new Z(sourceObj, data);
215     })) {
216       result = new Z(sourceObj, data);
217       result.attach(sourceObj);
218     } else static if (hasMember!(Z, "client")) {
219       result = new Z(__traits(getMember, sourceObj, "client"), data);
220       result.attach(sourceObj);
221     } else {
222       result = new Z;
223       result.deserializeFromJSON(data);
224     }
225     return result;
226   } else static if (isSomeString!Z) {
227     static if (__traits(compiles, {
228       return cast(Z)data.get!string;
229     })) {
230       return cast(Z)data.get!string;
231     } else {
232       return data.get!string.to!Z;
233     }
234   } else static if (isArray!Z) {
235     alias AT = ArrayElementType!(Z);
236 
237     foreach (obj; data) {
238       AT v = loadSingleField!(T, AT)(sourceObj, obj);
239       result ~= v;
240     }
241     return result;
242   } else static if (isAssociativeArray!Z) {
243     alias ArrayElementType!(typeof(result.keys)) Tk;
244     alias ArrayElementType!(typeof(result.values)) Tv;
245 
246     foreach (ref string k, ref v; data) {
247       Tv val = loadSingleField!(T, Tv)(sourceObj, v);
248 
249       result[k.to!Tk] = val;
250     }
251     return result;
252   } else static if (isIntegral!Z) {
253     if (data.type == VibeJSON.Type..string) {
254       return data.get!string.to!Z;
255     } else {
256       static if (__traits(compiles, { result = data.to!Z; })) {
257         return data.to!Z;
258       } else {
259         return data.get!Z;
260       }
261     }
262   } else {
263     return data.to!Z;
264   }
265 }
266 
267 private void attach(T, Z)(T baseObj, Z parentObj) {
268   foreach (fieldName; FieldNameTuple!T) {
269     alias FieldType = typeof(__traits(getMember, baseObj, fieldName));
270 
271     static if (is(FieldType == Z)) {
272       __traits(getMember, baseObj, fieldName) = parentObj;
273     }
274   }
275 }
276 
277 T deserializeFromJSON(T)(VibeJSON jsonData) {
278   T result = new T;
279   result.deserializeFromJSON(jsonData);
280   return result;
281 }
282 
283 T[] deserializeFromJSONArray(T)(VibeJSON jsonData, T delegate(VibeJSON) cons) {
284   T[] result;
285 
286   foreach (item; jsonData) {
287     result ~= cons(item);
288   }
289 
290   return result;
291 }
292 
293 unittest {
294   // Test Enums
295   enum TestEnum {
296     A = "A",
297     B = "B",
298   }
299 
300   class TestEnumClass {
301     TestEnum test;
302   }
303 
304   (new TestEnumClass()).deserializeFromJSON(
305     parseJsonString(q{{"test": "A"}}),
306   );
307 
308   // Test Timestamp
309   import std.datetime: SysTime;
310 
311   class TestTimestampClass {
312     @JSONTimestamp
313     SysTime timestamp;
314   }
315 
316   (new TestTimestampClass()).deserializeFromJSON(
317     parseJsonString(q{{"timestamp": "2018-11-13T02:51:57.736000+00:00"}}),
318   );
319 }