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 }