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