1 module mongoschema; 2 3 import core.time; 4 import std.traits; 5 import std.typecons : BitFlags, isTuple; 6 public import vibe.data.bson; 7 public import vibe.db.mongo.collection; 8 public import vibe.db.mongo.connection; 9 10 public import mongoschema.date; 11 public import mongoschema.db; 12 public import mongoschema.query; 13 public import mongoschema.variant; 14 15 // Bson Attributes 16 17 /// Will ignore the variables and not encode/decode them. 18 enum schemaIgnore; 19 /// Custom encode function. `func` is the name of the function which must be present as child. 20 struct encode 21 { /++ Function name (needs to be member function) +/ string func; 22 } 23 /// Custom decode function. `func` is the name of the function which must be present as child. 24 struct decode 25 { /++ Function name (needs to be member function) +/ string func; 26 } 27 /// Encodes the value as binary value. Must be an array with one byte wide elements. 28 struct binaryType 29 { /++ Type to encode +/ BsonBinData.Type type = BsonBinData.Type.generic; 30 } 31 /// Custom name for special characters. 32 struct schemaName 33 { /++ Custom replacement name +/ string name; 34 } 35 36 // Mongo Attributes 37 /// Will create an index with (by default) no flags. 38 enum mongoForceIndex; 39 /// Background index construction allows read and write operations to continue while building the index. 40 enum mongoBackground; 41 /// Drops duplicates in the database. Only for Mongo versions less than 3.0 42 enum mongoDropDuplicates; 43 /// Sparse indexes are like non-sparse indexes, except that they omit references to documents that do not include the indexed field. 44 enum mongoSparse; 45 /// MongoDB allows you to specify a unique constraint on an index. These constraints prevent applications from inserting documents that have duplicate values for the inserted fields. 46 enum mongoUnique; 47 /// TTL indexes expire documents after the specified number of seconds has passed since the indexed field value; i.e. the expiration threshold is the indexed field value plus the specified number of seconds. 48 /// Field must be a SchemaDate/BsonDate. You must update the time using collMod. 49 struct mongoExpire 50 { 51 /// 52 this(long seconds) 53 { 54 this.seconds = cast(ulong) seconds; 55 } 56 /// 57 this(ulong seconds) 58 { 59 this.seconds = seconds; 60 } 61 /// 62 this(Duration time) 63 { 64 seconds = cast(ulong) time.total!"msecs"; 65 } 66 /// 67 ulong seconds; 68 } 69 70 package template isVariable(alias T) 71 { 72 enum isVariable = !is(T) && is(typeof(T)) && !isCallable!T 73 && !is(T == void) && !__traits(isStaticFunction, T) && !__traits(isOverrideFunction, T) 74 && !__traits(isFinalFunction, T) && !__traits(isAbstractFunction, T) 75 && !__traits(isVirtualFunction, T) && !__traits(isVirtualMethod, 76 T) && !is(ReturnType!T); 77 } 78 79 package template isVariable(T) 80 { 81 enum isVariable = false; // Types are no variables 82 } 83 84 /// Converts any value to a bson value 85 Bson memberToBson(T)(T member) 86 { 87 static if (__traits(hasMember, T, "toBson") && is(ReturnType!(typeof(T.toBson)) == Bson)) 88 { 89 // Custom defined toBson 90 return T.toBson(member); 91 } 92 else static if (is(T == Json)) 93 { 94 return Bson.fromJson(member); 95 } 96 else static if (is(T == BsonBinData) || is(T == BsonObjectID) 97 || is(T == BsonDate) || is(T == BsonTimestamp) 98 || is(T == BsonRegex) || is(T == typeof(null))) 99 { 100 return Bson(member); 101 } 102 else static if (is(T == enum)) 103 { // Enum value 104 return Bson(cast(OriginalType!T) member); 105 } 106 else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe)) 107 { // std.typecons.BitFlags 108 return Bson(cast(OriginalType!Enum) member); 109 } 110 else static if (isArray!(T) && !isSomeString!T || isTuple!T) 111 { // Arrays of anything except strings 112 Bson[] values; 113 foreach (val; member) 114 values ~= memberToBson(val); 115 return Bson(values); 116 } 117 else static if (isAssociativeArray!T) 118 { // Associative Arrays (Objects) 119 Bson[string] values; 120 static assert(is(KeyType!T == string), "Associative arrays must have strings as keys"); 121 foreach (string name, val; member) 122 values[name] = memberToBson(val); 123 return Bson(values); 124 } 125 else static if (is(T == Bson)) 126 { // Already a Bson object 127 return member; 128 } 129 else static if (__traits(compiles, { Bson(member); })) 130 { // Check if this can be passed 131 return Bson(member); 132 } 133 else static if (!isBasicType!T) 134 { 135 // Mixed in MongoSchema 136 return member.toSchemaBson(); 137 } 138 else // Generic value 139 { 140 pragma(msg, "Warning falling back to serializeToBson for type " ~ T.stringof); 141 return serializeToBson(member); 142 } 143 } 144 145 /// Converts any bson value to a given type 146 T bsonToMember(T)(auto ref T member, Bson value) 147 { 148 static if (__traits(hasMember, T, "fromBson") && is(ReturnType!(typeof(T.fromBson)) == T)) 149 { 150 // Custom defined toBson 151 return T.fromBson(value); 152 } 153 else static if (is(T == Json)) 154 { 155 return Bson.fromJson(value); 156 } 157 else static if (is(T == BsonBinData) || is(T == BsonObjectID) 158 || is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex)) 159 { 160 return value.get!T; 161 } 162 else static if (is(T == enum)) 163 { // Enum value 164 return cast(T) value.get!(OriginalType!T); 165 } 166 else static if (is(T == BitFlags!(Enum, Unsafe), Enum, alias Unsafe)) 167 { // std.typecons.BitFlags 168 return cast(T) cast(Enum) value.get!(OriginalType!Enum); 169 } 170 else static if (isTuple!T) 171 { // Tuples 172 auto bsons = value.get!(Bson[]); 173 T values; 174 foreach (i, val; values) 175 { 176 values[i] = bsonToMember!(typeof(val))(values[i], bsons[i]); 177 } 178 return values; 179 } 180 else static if (isArray!T && !isSomeString!T) 181 { // Arrays of anything except strings 182 alias Type = typeof(member[0]); 183 T values; 184 foreach (val; value) 185 { 186 values ~= bsonToMember!Type(Type.init, val); 187 } 188 return values; 189 } 190 else static if (isAssociativeArray!T) 191 { // Associative Arrays (Objects) 192 T values; 193 static assert(is(KeyType!T == string), "Associative arrays must have strings as keys"); 194 alias ValType = ValueType!T; 195 foreach (string name, val; value) 196 values[name] = bsonToMember!ValType(ValType.init, val); 197 return values; 198 } 199 else static if (is(T == Bson)) 200 { // Already a Bson object 201 return value; 202 } 203 else static if (__traits(compiles, { value.get!T(); })) 204 { // Check if this can be passed 205 static if (is(T == long)) 206 { // If the output expects a long, but the data is a int, do .get!int 207 if (value.type == Bson.Type.int_) 208 return value.get!int(); 209 } 210 return value.get!T(); 211 } 212 else static if (!isBasicType!T) 213 { 214 // Mixed in MongoSchema 215 return value.fromSchemaBson!T(); 216 } 217 else // Generic value 218 { 219 pragma(msg, "Warning falling back to deserializeBson for type " ~ T.stringof); 220 return deserializeBson!T(value); 221 } 222 } 223 224 /// Generates a Bson document from a struct/class object 225 Bson toSchemaBson(T)(T obj) 226 { 227 static if (__traits(compiles, cast(T) null) && __traits(compiles, { 228 T foo = null; 229 })) 230 { 231 if (obj is null) 232 return Bson(null); 233 } 234 235 Bson data = Bson.emptyObject; 236 237 static if (hasMember!(T, "_schema_object_id_")) 238 { 239 if (obj.bsonID.valid) 240 data["_id"] = obj.bsonID; 241 } 242 243 foreach (memberName; __traits(allMembers, T)) 244 { 245 static if (memberName == "_schema_object_id_") 246 continue; 247 else static if (__traits(compiles, { 248 static s = isVariable!(__traits(getMember, obj, memberName)); 249 }) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, { 250 static s = __traits(getMember, T, memberName); 251 }) // No static members 252 && __traits(compiles, { 253 typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, 254 obj, memberName); 255 })) 256 { 257 static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 258 { 259 string name = memberName; 260 Bson value; 261 static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 262 { 263 static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 264 { 265 static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 266 .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 267 name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 268 } 269 270 static if (hasUDA!((__traits(getMember, obj, memberName)), encode)) 271 { 272 static assert(getUDAs!((__traits(getMember, obj, memberName)), encode).length == 1, 273 "Member '" ~ memberName ~ "' can only have one encoder!"); 274 mixin("value = obj." ~ getUDAs!((__traits(getMember, 275 obj, memberName)), encode)[0].func ~ "(obj);"); 276 } 277 else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 278 { 279 static assert(isArray!(typeof((__traits(getMember, obj, 280 memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 281 "Binary member '" ~ memberName 282 ~ "' can only be an array of 1 byte values"); 283 static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1, 284 "Binary member '" ~ memberName ~ "' can only have one type!"); 285 BsonBinData.Type type = getUDAs!((__traits(getMember, 286 obj, memberName)), binaryType)[0].type; 287 value = Bson(BsonBinData(type, 288 cast(immutable(ubyte)[])(__traits(getMember, obj, memberName)))); 289 } 290 else 291 { 292 static if (__traits(compiles, { 293 __traits(hasMember, typeof((__traits(getMember, 294 obj, memberName))), "toBson"); 295 }) && __traits(hasMember, typeof((__traits(getMember, obj, 296 memberName))), "toBson") && !is(ReturnType!(typeof((__traits(getMember, 297 obj, memberName)).toBson)) == Bson)) 298 pragma(msg, "Warning: ", typeof((__traits(getMember, obj, memberName))).stringof, 299 ".toBson does not return a vibe.data.bson.Bson struct!"); 300 301 value = memberToBson(__traits(getMember, obj, memberName)); 302 } 303 data[name] = value; 304 } 305 } 306 } 307 } 308 309 return data; 310 } 311 312 /// Generates a struct/class object from a Bson node 313 T fromSchemaBson(T)(Bson bson) 314 { 315 static if (__traits(compiles, cast(T) null) && __traits(compiles, { 316 T foo = null; 317 })) 318 { 319 if (bson.isNull) 320 return null; 321 } 322 T obj = T.init; 323 324 static if (hasMember!(T, "_schema_object_id_")) 325 { 326 if (!bson.tryIndex("_id").isNull) 327 obj.bsonID = bson["_id"].get!BsonObjectID; 328 } 329 330 foreach (memberName; __traits(allMembers, T)) 331 { 332 static if (memberName == "_schema_object_id_") 333 continue; 334 else static if (__traits(compiles, { 335 static s = isVariable!(__traits(getMember, obj, memberName)); 336 }) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, { 337 static s = __traits(getMember, T, memberName); 338 }) // No static members 339 && __traits(compiles, { 340 typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, 341 obj, memberName); 342 })) 343 { 344 static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public") 345 { 346 string name = memberName; 347 static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore)) 348 { 349 static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName)) 350 { 351 static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName) 352 .length == 1, "Member '" ~ memberName ~ "' can only have one name!"); 353 name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name; 354 } 355 356 // compile time code will still be generated but not run at runtime 357 if (bson.tryIndex(name).isNull || bson[name].type == Bson.Type.undefined) 358 continue; 359 360 static if (hasUDA!((__traits(getMember, obj, memberName)), decode)) 361 { 362 static assert(getUDAs!((__traits(getMember, obj, memberName)), decode).length == 1, 363 "Member '" ~ memberName ~ "' can only have one decoder!"); 364 mixin("obj." ~ memberName ~ " = obj." ~ getUDAs!((__traits(getMember, 365 obj, memberName)), decode)[0].func ~ "(bson);"); 366 } 367 else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType)) 368 { 369 static assert(isArray!(typeof((__traits(getMember, obj, 370 memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1, 371 "Binary member '" ~ memberName 372 ~ "' can only be an array of 1 byte values"); 373 static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1, 374 "Binary member '" ~ memberName ~ "' can only have one type!"); 375 assert(bson[name].type == Bson.Type.binData); 376 auto data = bson[name].get!(BsonBinData).rawData; 377 mixin("obj." ~ memberName ~ " = cast(typeof(obj." ~ memberName ~ ")) data;"); 378 } 379 else 380 { 381 mixin( 382 "obj." ~ memberName ~ " = bsonToMember(obj." 383 ~ memberName ~ ", bson[name]);"); 384 } 385 } 386 } 387 } 388 } 389 390 return obj; 391 }