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