1 module mongoschema;
2 
3 import vibe.data.bson;
4 import vibe.db.mongo.collection;
5 import vibe.db.mongo.connection;
6 import std.datetime;
7 import std.traits;
8 import core.time;
9 import std.typecons : tuple, Nullable;
10 
11 // Bson Attributes
12 
13 /// Will ignore the variables and not encode/decode them.
14 enum schemaIgnore;
15 /// Custom encode function. `func` is the name of the function which must be present as child.
16 struct encode
17 { /++ Function name (needs to be member function) +/ string func;
18 }
19 /// Custom decode function. `func` is the name of the function which must be present as child.
20 struct decode
21 { /++ Function name (needs to be member function) +/ string func;
22 }
23 /// Encodes the value as binary value. Must be an array with one byte wide elements.
24 struct binaryType
25 { /++ Type to encode +/ BsonBinData.Type type = BsonBinData.Type.generic;
26 }
27 /// Custom name for special characters.
28 struct schemaName
29 { /++ Custom replacement name +/ string name;
30 }
31 
32 // Mongo Attributes
33 /// Will create an index with (by default) no flags.
34 enum mongoForceIndex;
35 /// Background index construction allows read and write operations to continue while building the index.
36 enum mongoBackground;
37 /// Drops duplicates in the database. Only for Mongo versions less than 3.0
38 enum mongoDropDuplicates;
39 /// Sparse indexes are like non-sparse indexes, except that they omit references to documents that do not include the indexed field.
40 enum mongoSparse;
41 /// 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.
42 enum mongoUnique;
43 /// 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.
44 /// Field must be a SchemaDate/BsonDate. You must update the time using collMod.
45 struct mongoExpire
46 {
47 	///
48 	this(long seconds)
49 	{
50 		this.seconds = cast(ulong) seconds;
51 	}
52 	///
53 	this(ulong seconds)
54 	{
55 		this.seconds = seconds;
56 	}
57 	///
58 	this(Duration time)
59 	{
60 		seconds = cast(ulong) time.total!"msecs";
61 	}
62 	///
63 	ulong seconds;
64 }
65 
66 private template isVariable(alias T)
67 {
68 	enum isVariable = !is(T) && is(typeof(T)) && !isCallable!T
69 			&& !is(T == void) && !__traits(isStaticFunction, T) && !__traits(isOverrideFunction, T)
70 			&& !__traits(isFinalFunction, T) && !__traits(isAbstractFunction, T)
71 			&& !__traits(isVirtualFunction, T) && !__traits(isVirtualMethod,
72 					T) && !is(ReturnType!T);
73 }
74 
75 private template isVariable(T)
76 {
77 	enum isVariable = false; // Types are no variables
78 }
79 
80 private template getUDAs(alias symbol, alias attribute)
81 {
82 	import std.typetuple : Filter;
83 
84 	enum isDesiredUDA(alias S) = is(typeof(S) == attribute);
85 	alias getUDAs = Filter!(isDesiredUDA, __traits(getAttributes, symbol));
86 }
87 
88 private Bson memberToBson(T)(T member)
89 {
90 	static if (__traits(hasMember, T, "toBson") && is(ReturnType!(typeof(T.toBson)) == Bson))
91 	{
92 		// Custom defined toBson
93 		return T.toBson(member);
94 	}
95 	else static if (is(T == Json))
96 	{
97 		return Bson.fromJson(member);
98 	}
99 	else static if (is(T == BsonBinData) || is(T == BsonObjectID)
100 			|| is(T == BsonDate) || is(T == BsonTimestamp)
101 			|| is(T == BsonRegex) || is(T == typeof(null)))
102 	{
103 		return Bson(member);
104 	}
105 	else static if (!isBasicType!T && !isArray!T && !is(T == enum)
106 			&& !is(T == Bson) && !isAssociativeArray!T)
107 	{
108 		// Mixed in MongoSchema
109 		return member.toSchemaBson();
110 	}
111 	else // Generic value
112 	{
113 		static if (is(T == enum))
114 		{ // Enum value
115 			return Bson(cast(OriginalType!T) member);
116 		}
117 		else static if (isArray!(T) && !isSomeString!T)
118 		{ // Arrays of anything except strings
119 			Bson[] values;
120 			foreach (val; member)
121 				values ~= memberToBson(val);
122 			return Bson(values);
123 		}
124 		else static if (isAssociativeArray!T)
125 		{ // Associative Arrays (Objects)
126 			Bson[string] values;
127 			foreach (name, val; member)
128 				values[name] = memberToBson(val);
129 			return Bson(values);
130 		}
131 		else static if (is(T == Bson))
132 		{ // Already a Bson object
133 			return member;
134 		}
135 		else static if (__traits(compiles, { Bson(member); }))
136 		{ // Check if this can be passed
137 			return Bson(member);
138 		}
139 		else
140 		{
141 			pragma(msg, "Warning falling back to serializeToBson for type " ~ T.stringof);
142 			return serializeToBson(member);
143 		}
144 	}
145 }
146 
147 private T bsonToMember(T)(auto ref T member, Bson value)
148 {
149 	static if (__traits(hasMember, T, "fromBson") && is(ReturnType!(typeof(T.fromBson)) == T))
150 	{
151 		// Custom defined toBson
152 		return T.fromBson(value);
153 	}
154 	else static if (is(T == Json))
155 	{
156 		return Bson.fromJson(value);
157 	}
158 	else static if (is(T == BsonBinData) || is(T == BsonObjectID)
159 			|| is(T == BsonDate) || is(T == BsonTimestamp) || is(T == BsonRegex))
160 	{
161 		return value.get!T;
162 	}
163 	else static if (!isBasicType!T && !isArray!T && !is(T == enum)
164 			&& !is(T == Bson) && !isAssociativeArray!T)
165 	{
166 		// Mixed in MongoSchema
167 		return value.fromSchemaBson!T();
168 	}
169 	else // Generic value
170 	{
171 		static if (is(T == enum))
172 		{ // Enum value
173 			return cast(T) value.get!(OriginalType!T);
174 		}
175 		else static if (isArray!T && !isSomeString!T)
176 		{ // Arrays of anything except strings
177 			alias Type = typeof(member[0]);
178 			T values;
179 			foreach (val; value)
180 			{
181 				values ~= bsonToMember!Type(Type.init, val);
182 			}
183 			return values;
184 		}
185 		else static if (isAssociativeArray!T)
186 		{ // Associative Arrays (Objects)
187 			T values;
188 			alias ValType = ValueType!T;
189 			foreach (name, val; value)
190 				values[name] = bsonToMember!ValType(ValType.init, val);
191 			return values;
192 		}
193 		else static if (is(T == Bson))
194 		{ // Already a Bson object
195 			return value;
196 		}
197 		else static if (__traits(compiles, { value.get!T(); }))
198 		{ // Check if this can be passed
199 			return value.get!T();
200 		}
201 		else
202 		{
203 			pragma(msg, "Warning falling back to deserializeBson for type " ~ T.stringof);
204 			return deserializeBson!T(value);
205 		}
206 	}
207 }
208 
209 /// Generates a Bson document from a struct/class object
210 Bson toSchemaBson(T)(T obj)
211 {
212 	static if (__traits(compiles, cast(T) null) && __traits(compiles, {
213 			T foo = null;
214 		}))
215 	{
216 		if (obj is null)
217 			return Bson(null);
218 	}
219 
220 	Bson data = Bson.emptyObject;
221 
222 	static if (hasMember!(T, "_schema_object_id_"))
223 	{
224 		if (obj.bsonID.valid)
225 			data["_id"] = obj.bsonID;
226 	}
227 
228 	foreach (memberName; __traits(allMembers, T))
229 	{
230 		static if (memberName == "_schema_object_id_")
231 			continue;
232 		else static if (__traits(compiles, {
233 				static s = isVariable!(__traits(getMember, obj, memberName));
234 			}) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, {
235 				static s = __traits(getMember, T, memberName);
236 			}) // No static members
237 			 && __traits(compiles, {
238 				typeof(__traits(getMember, obj, memberName)) t = __traits(getMember,
239 				obj, memberName);
240 			}))
241 		{
242 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
243 			{
244 				string name = memberName;
245 				Bson value;
246 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
247 				{
248 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
249 					{
250 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
251 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
252 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
253 					}
254 
255 					static if (hasUDA!((__traits(getMember, obj, memberName)), encode))
256 					{
257 						static assert(getUDAs!((__traits(getMember, obj, memberName)), encode).length == 1,
258 								"Member '" ~ memberName ~ "' can only have one encoder!");
259 						mixin("value = obj." ~ getUDAs!((__traits(getMember,
260 								obj, memberName)), encode)[0].func ~ "(obj);");
261 					}
262 					else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType))
263 					{
264 						static assert(isArray!(typeof((__traits(getMember, obj,
265 								memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1,
266 								"Binary member '" ~ memberName
267 								~ "' can only be an array of 1 byte values");
268 						static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1,
269 								"Binary member '" ~ memberName ~ "' can only have one type!");
270 						BsonBinData.Type type = getUDAs!((__traits(getMember,
271 								obj, memberName)), binaryType)[0].type;
272 						value = Bson(BsonBinData(type,
273 								cast(immutable(ubyte)[])(__traits(getMember, obj, memberName))));
274 					}
275 					else
276 					{
277 						static if (__traits(compiles, {
278 								__traits(hasMember, typeof((__traits(getMember,
279 								obj, memberName))), "toBson");
280 							}) && __traits(hasMember, typeof((__traits(getMember, obj,
281 								memberName))), "toBson") && !is(ReturnType!(typeof((__traits(getMember,
282 								obj, memberName)).toBson)) == Bson))
283 							pragma(msg, "Warning: ", typeof((__traits(getMember, obj, memberName))).stringof,
284 									".toBson does not return a vibe.data.bson.Bson struct!");
285 
286 						value = memberToBson(__traits(getMember, obj, memberName));
287 					}
288 					data[name] = value;
289 				}
290 			}
291 		}
292 	}
293 
294 	return data;
295 }
296 
297 /// Generates a struct/class object from a Bson node
298 T fromSchemaBson(T)(Bson bson)
299 {
300 	static if (__traits(compiles, cast(T) null) && __traits(compiles, {
301 			T foo = null;
302 		}))
303 	{
304 		if (bson.isNull)
305 			return null;
306 	}
307 	T obj = T.init;
308 
309 	static if (hasMember!(T, "_schema_object_id_"))
310 	{
311 		if (!bson.tryIndex("_id").isNull)
312 			obj.bsonID = bson["_id"].get!BsonObjectID;
313 	}
314 
315 	foreach (memberName; __traits(allMembers, T))
316 	{
317 		static if (memberName == "_schema_object_id_")
318 			continue;
319 		else static if (__traits(compiles, {
320 				static s = isVariable!(__traits(getMember, obj, memberName));
321 			}) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, {
322 				static s = __traits(getMember, T, memberName);
323 			}) // No static members
324 			 && __traits(compiles, {
325 				typeof(__traits(getMember, obj, memberName)) t = __traits(getMember,
326 				obj, memberName);
327 			}))
328 		{
329 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
330 			{
331 				string name = memberName;
332 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
333 				{
334 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
335 					{
336 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
337 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
338 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
339 					}
340 
341 					// compile time code will still be generated but not run at runtime
342 					if (bson.tryIndex(name).isNull)
343 						continue;
344 
345 					static if (hasUDA!((__traits(getMember, obj, memberName)), decode))
346 					{
347 						static assert(getUDAs!((__traits(getMember, obj, memberName)), decode).length == 1,
348 								"Member '" ~ memberName ~ "' can only have one decoder!");
349 						mixin("obj." ~ memberName ~ " = obj." ~ getUDAs!((__traits(getMember,
350 								obj, memberName)), decode)[0].func ~ "(bson);");
351 					}
352 					else static if (hasUDA!((__traits(getMember, obj, memberName)), binaryType))
353 					{
354 						static assert(isArray!(typeof((__traits(getMember, obj,
355 								memberName)))) && typeof((__traits(getMember, obj, memberName))[0]).sizeof == 1,
356 								"Binary member '" ~ memberName
357 								~ "' can only be an array of 1 byte values");
358 						static assert(getUDAs!((__traits(getMember, obj, memberName)), binaryType).length == 1,
359 								"Binary member '" ~ memberName ~ "' can only have one type!");
360 						assert(bson[name].type == Bson.Type.binData);
361 						auto data = bson[name].get!(BsonBinData).rawData;
362 						mixin("obj." ~ memberName ~ " = cast(typeof(obj." ~ memberName ~ ")) data;");
363 					}
364 					else
365 					{
366 						mixin(
367 								"obj." ~ memberName ~ " = bsonToMember(obj."
368 								~ memberName ~ ", bson[name]);");
369 					}
370 				}
371 			}
372 		}
373 	}
374 
375 	return obj;
376 }
377 
378 struct DocumentRange(Schema)
379 {
380 	private MongoCursor!(Bson, Bson, typeof(null)) _cursor;
381 
382 	package this(MongoCursor!(Bson, Bson, typeof(null)) cursor)
383 	{
384 		_cursor = cursor;
385 	}
386 
387 	/**
388 		Returns true if there are no more documents for this cursor.
389 
390 		Throws: An exception if there is a query or communication error.
391 	*/
392 	@property bool empty()
393 	{
394 		return _cursor.empty;
395 	}
396 
397 	/**
398 		Returns the current document of the response.
399 
400 		Use empty and popFront to iterate over the list of documents using an
401 		input range interface. Note that calling this function is only allowed
402 		if empty returns false.
403 	*/
404 	@property Schema front()
405 	{
406 		return fromSchemaBson!Schema(_cursor.front);
407 	}
408 
409 	/**
410 		Controls the order in which the query returns matching documents.
411 
412 		This method must be called before starting to iterate, or an exeption
413 		will be thrown. If multiple calls to $(D sort()) are issued, only
414 		the last one will have an effect.
415 
416 		Params:
417 			order = A BSON object convertible value that defines the sort order
418 				of the result. This BSON object must be structured according to
419 				the MongoDB documentation (see below).
420 
421 		Returns: Reference to the modified original curser instance.
422 
423 		Throws:
424 			An exception if there is a query or communication error.
425 			Also throws if the method was called after beginning of iteration.
426 
427 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.sort)
428 	*/
429 	auto sort(T)(T order)
430 	{
431 		_cursor.sort(serializeToBson(order));
432 		return this;
433 	}
434 
435 	/**
436 		Limits the number of documents that the cursor returns.
437 
438 		This method must be called before beginnig iteration in order to have
439 		effect. If multiple calls to limit() are made, the one with the lowest
440 		limit will be chosen.
441 
442 		Params:
443 			count = The maximum number number of documents to return. A value
444 				of zero means unlimited.
445 
446 		Returns: the same cursor
447 
448 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.limit)
449 	*/
450 	auto limit(size_t count)
451 	{
452 		_cursor.limit(count);
453 		return this;
454 	}
455 
456 	/**
457 		Skips a given number of elements at the beginning of the cursor.
458 
459 		This method must be called before beginnig iteration in order to have
460 		effect. If multiple calls to skip() are made, the one with the maximum
461 		number will be chosen.
462 
463 		Params:
464 			count = The number of documents to skip.
465 
466 		Returns: the same cursor
467 
468 		See_Also: $(LINK http://docs.mongodb.org/manual/reference/method/cursor.skip)
469 	*/
470 	auto skip(int count)
471 	{
472 		_cursor.skip(count);
473 		return this;
474 	}
475 
476 	/**
477 		Advances the cursor to the next document of the response.
478 
479 		Note that calling this function is only allowed if empty returns false.
480 	*/
481 	void popFront()
482 	{
483 		_cursor.popFront();
484 	}
485 
486 	/**
487 		Iterates over all remaining documents.
488 
489 		Note that iteration is one-way - elements that have already been visited
490 		will not be visited again if another iteration is done.
491 
492 		Throws: An exception if there is a query or communication error.
493 	*/
494 	int opApply(int delegate(Schema doc) del)
495 	{
496 		while (!_cursor.empty)
497 		{
498 			auto doc = _cursor.front;
499 			_cursor.popFront();
500 			if (auto ret = del(fromSchemaBson!Schema(doc)))
501 				return ret;
502 		}
503 		return 0;
504 	}
505 }
506 
507 class DocumentNotFoundException : Exception
508 {
509 	this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow @nogc @safe
510 	{
511 		super(msg, file, line);
512 	}
513 }
514 
515 /// Mixin for functions for interacting with Mongo collections.
516 mixin template MongoSchema()
517 {
518 	static MongoCollection _schema_collection_;
519 	private BsonObjectID _schema_object_id_;
520 
521 	/// Returns: the _id value (if set by save or find)
522 	@property ref BsonObjectID bsonID()
523 	{
524 		return _schema_object_id_;
525 	}
526 
527 	/// Inserts or updates an existing value.
528 	void save()
529 	{
530 		if (_schema_object_id_.valid)
531 		{
532 			_schema_collection_.update(Bson(["_id" : Bson(_schema_object_id_)]),
533 					this.toSchemaBson(), UpdateFlags.upsert);
534 		}
535 		else
536 		{
537 			_schema_object_id_ = BsonObjectID.generate;
538 			auto bson = this.toSchemaBson();
539 			_schema_collection_.insert(bson);
540 		}
541 	}
542 
543 	/// Removes this object from the collection. Returns false when _id of this is not set.
544 	bool remove()
545 	{
546 		if (!_schema_object_id_.valid)
547 			return false;
548 		_schema_collection_.remove(Bson(["_id" : Bson(_schema_object_id_)]),
549 				DeleteFlags.SingleRemove);
550 		return true;
551 	}
552 
553 	static auto findOneOrThrow(T)(T query)
554 	{
555 		Bson found = _schema_collection_.findOne(query);
556 		if (found.isNull)
557 			throw new DocumentNotFoundException("Could not find one " ~ typeof(this).stringof);
558 		return found;
559 	}
560 
561 	/// Finds one element with the object id `id`
562 	static typeof(this) findById(BsonObjectID id)
563 	{
564 		return fromSchemaBson!(typeof(this))(findOneOrThrow(Bson(["_id" : Bson(id)])));
565 	}
566 
567 	/// Finds one element with the hex id `id`
568 	static typeof(this) findById(string id)
569 	{
570 		return findById(BsonObjectID.fromString(id));
571 	}
572 
573 	/// Finds one element using a query.
574 	static typeof(this) findOne(T)(T query)
575 	{
576 		return fromSchemaBson!(typeof(this))(findOneOrThrow(query));
577 	}
578 
579 	static Nullable!(typeof(this)) tryFindById(BsonObjectID id)
580 	{
581 		Bson found = _schema_collection_.findOne(Bson(["_id" : Bson(id)]));
582 		if (found.isNull)
583 			return Nullable!(typeof(this)).init;
584 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
585 	}
586 
587 	static Nullable!(typeof(this)) tryFindById(string id)
588 	{
589 		return tryFindById(BsonObjectID.fromString(id));
590 	}
591 
592 	static Nullable!(typeof(this)) tryFindOne(T)(T query)
593 	{
594 		Bson found = _schema_collection_.findOne(query);
595 		if (found.isNull)
596 			return Nullable!(typeof(this)).init;
597 		return Nullable!(typeof(this))(fromSchemaBson!(typeof(this))(found));
598 	}
599 
600 	/// Finds one or more elements using a query.
601 	static typeof(this)[] find(T)(T query, QueryFlags flags = QueryFlags.None,
602 			int num_skip = 0, int num_docs_per_chunk = 0)
603 	{
604 		typeof(this)[] values;
605 		foreach (entry; _schema_collection_.find(query, null, flags, num_skip, num_docs_per_chunk))
606 		{
607 			values ~= fromSchemaBson!(typeof(this))(entry);
608 		}
609 		return values;
610 	}
611 
612 	/// Queries all elements from the collection.
613 	deprecated("use findAll instead") static typeof(this)[] find()
614 	{
615 		typeof(this)[] values;
616 		foreach (entry; _schema_collection_.find())
617 		{
618 			values ~= fromSchemaBson!(typeof(this))(entry);
619 		}
620 		return values;
621 	}
622 
623 	/// Finds one or more elements using a query as range.
624 	static DocumentRange!(typeof(this)) findRange(T)(T query,
625 			QueryFlags flags = QueryFlags.None, int num_skip = 0, int num_docs_per_chunk = 0)
626 	{
627 		return DocumentRange!(typeof(this))(_schema_collection_.find(query,
628 				null, flags, num_skip, num_docs_per_chunk));
629 	}
630 
631 	/// Queries all elements from the collection as range.
632 	static DocumentRange!(typeof(this)) findAll()
633 	{
634 		return DocumentRange!(typeof(this))(_schema_collection_.find());
635 	}
636 
637 	/// Updates a document.
638 	static void update(T, U)(T query, U update, UpdateFlags options = UpdateFlags.none)
639 	{
640 		_schema_collection_.update(query, update, options);
641 	}
642 
643 	/// Updates a document or inserts it when not existent. Shorthand for `update(..., UpdateFlags.upsert)`
644 	static void upsert(T, U)(T query, U update, UpdateFlags options = UpdateFlags.upsert)
645 	{
646 		_schema_collection_.update(query, update, options);
647 	}
648 }
649 
650 /// Binds a MongoCollection to a Schema. Can only be done once!
651 void register(T)(MongoCollection collection)
652 {
653 	T obj = T.init;
654 
655 	static if (hasMember!(T, "_schema_collection_"))
656 	{
657 		assert(T._schema_collection_.name.length == 0, "Can't register a Schema to 2 collections!");
658 		T._schema_collection_ = collection;
659 	}
660 
661 	foreach (memberName; __traits(allMembers, T))
662 	{
663 		static if (__traits(compiles, {
664 				static s = isVariable!(__traits(getMember, obj, memberName));
665 			}) && isVariable!(__traits(getMember, obj, memberName)))
666 		{
667 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
668 			{
669 				string name = memberName;
670 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
671 				{
672 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
673 					{
674 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
675 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
676 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
677 					}
678 
679 					IndexFlags flags = IndexFlags.None;
680 					ulong expires = 0LU;
681 					bool force;
682 
683 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoForceIndex))
684 					{
685 						force = true;
686 					}
687 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoBackground))
688 					{
689 						flags |= IndexFlags.Background;
690 					}
691 					static if (hasUDA!((__traits(getMember, obj, memberName)),
692 							mongoDropDuplicates))
693 					{
694 						flags |= IndexFlags.DropDuplicates;
695 					}
696 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoSparse))
697 					{
698 						flags |= IndexFlags.Sparse;
699 					}
700 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoUnique))
701 					{
702 						flags |= IndexFlags.Unique;
703 					}
704 					static if (hasUDA!((__traits(getMember, obj, memberName)), mongoExpire))
705 					{
706 						static assert(getUDAs!((__traits(getMember, obj, memberName)), mongoExpire).length == 1,
707 								"Member '" ~ memberName ~ "' can only have one expiry value!");
708 						flags |= IndexFlags.ExpireAfterSeconds;
709 						expires = getUDAs!((__traits(getMember, obj, memberName)), mongoExpire)[0]
710 							.seconds;
711 					}
712 
713 					if (flags != IndexFlags.None || force)
714 						collection.ensureIndex([tuple(name, 1)], flags, dur!"seconds"(expires));
715 				}
716 			}
717 		}
718 	}
719 }
720 
721 /// Class serializing to a bson date containing a special `now` value that gets translated to the current time when converting to bson.
722 final struct SchemaDate
723 {
724 public:
725 	///
726 	this(BsonDate date)
727 	{
728 		_time = date.value;
729 	}
730 
731 	///
732 	this(long time)
733 	{
734 		_time = time;
735 	}
736 
737 	///
738 	@property auto time()
739 	{
740 		return _time;
741 	}
742 
743 	///
744 	static Bson toBson(SchemaDate date)
745 	{
746 		if (date._time == -1)
747 		{
748 			return Bson(BsonDate.fromStdTime(Clock.currStdTime()));
749 		}
750 		else
751 		{
752 			return Bson(BsonDate(date._time));
753 		}
754 	}
755 
756 	///
757 	static SchemaDate fromBson(Bson bson)
758 	{
759 		return SchemaDate(bson.get!BsonDate.value);
760 	}
761 
762 	/// Magic value setting the date to the current time stamp when serializing.
763 	static SchemaDate now()
764 	{
765 		return SchemaDate(-1);
766 	}
767 
768 private:
769 	long _time;
770 }
771 
772 unittest
773 {
774 	struct C
775 	{
776 		int a = 4;
777 	}
778 
779 	struct B
780 	{
781 		C cref;
782 	}
783 
784 	struct A
785 	{
786 		B bref;
787 	}
788 
789 	A a;
790 	a.bref.cref.a = 5;
791 	auto bson = a.toSchemaBson();
792 	assert(bson["bref"]["cref"]["a"].get!int == 5);
793 	A b = bson.fromSchemaBson!A();
794 	assert(b.bref.cref.a == 5);
795 }
796 
797 unittest
798 {
799 	import std.digest.digest;
800 	import std.digest.sha;
801 
802 	enum Activity
803 	{
804 		High,
805 		Medium,
806 		Low
807 	}
808 
809 	struct UserSchema
810 	{
811 		string username = "Unnamed";
812 		@binaryType()
813 		string salt = "foobar";
814 		@encode("encodePassword")
815 		@binaryType()
816 		string password;
817 		@schemaName("date-created")
818 		SchemaDate dateCreated = SchemaDate.now;
819 		Activity activity = Activity.Medium;
820 
821 		Bson encodePassword(UserSchema user)
822 		{
823 			// TODO: Replace with something more secure
824 			return Bson(BsonBinData(BsonBinData.Type.generic, sha1Of(user.password ~ user.salt)));
825 		}
826 	}
827 
828 	auto user = UserSchema();
829 	user.password = "12345";
830 	user.username = "Bob";
831 	auto bson = user.toSchemaBson();
832 	assert(bson["username"].get!string == "Bob");
833 	assert(bson["date-created"].get!(BsonDate).value > 0);
834 	assert(bson["activity"].get!(int) == cast(int) Activity.Medium);
835 	assert(bson["salt"].get!(BsonBinData).rawData == cast(ubyte[]) "foobar");
836 	assert(bson["password"].get!(BsonBinData).rawData == sha1Of(user.password ~ user.salt));
837 
838 	auto user2 = bson.fromSchemaBson!UserSchema();
839 	assert(user2.username == user.username);
840 	assert(user2.password != user.password);
841 	assert(user2.salt == user.salt);
842 	// dates are gonna differ as `user2` has the current time now and `user` a magic value to get the current time
843 	assert(user2.dateCreated != user.dateCreated);
844 	assert(user2.activity == user.activity);
845 }
846 
847 unittest
848 {
849 	import vibe.db.mongo.mongo;
850 	import std.digest.sha;
851 	import std.exception;
852 	import std.array;
853 
854 	auto client = connectMongoDB("localhost");
855 	auto database = client.getDatabase("test");
856 	MongoCollection users = database["users"];
857 	users.remove(); // Clears collection
858 
859 	struct User
860 	{
861 		mixin MongoSchema;
862 
863 		@mongoUnique string username;
864 		@binaryType()
865 		ubyte[] hash;
866 		@schemaName("profile-picture")
867 		string profilePicture;
868 		auto registered = SchemaDate.now;
869 	}
870 
871 	users.register!User;
872 
873 	assert(User.findAll().array.length == 0);
874 
875 	User user;
876 	user.username = "Example";
877 	user.hash = sha512Of("password123");
878 	user.profilePicture = "example-avatar.png";
879 
880 	assertNotThrown(user.save());
881 
882 	User user2;
883 	user2.username = "Bob";
884 	user2.hash = sha512Of("foobar");
885 	user2.profilePicture = "bob-avatar.png";
886 
887 	assertNotThrown(user2.save());
888 
889 	User faker;
890 	faker.username = "Example";
891 	faker.hash = sha512Of("PASSWORD");
892 	faker.profilePicture = "example-avatar.png";
893 
894 	assertThrown(faker.save());
895 	// Unique username
896 
897 	faker.username = "Example_";
898 	assertNotThrown(faker.save());
899 
900 	user.username = "NewExample";
901 	user.save();
902 
903 	auto actualFakeID = faker.bsonID;
904 	faker = User.findOne(["username" : "NewExample"]);
905 
906 	assert(actualFakeID != faker.bsonID);
907 
908 	foreach (usr; User.findAll)
909 	{
910 		usr.profilePicture = "default.png"; // Reset all profile pictures
911 		usr.save();
912 	}
913 	user = User.findOne(["username" : "NewExample"]);
914 	user2 = User.findOne(["username" : "Bob"]);
915 	faker = User.findOne(["username" : "Example_"]);
916 	assert(user.profilePicture == user2.profilePicture
917 			&& user2.profilePicture == faker.profilePicture && faker.profilePicture == "default.png");
918 
919 	User user3;
920 	user3.username = "User123";
921 	user3.hash = sha512Of("486951");
922 	user3.profilePicture = "new.png";
923 	User.upsert(["username" : "User123"], user3.toSchemaBson);
924 	user3 = User.findOne(["username" : "User123"]);
925 	assert(user3.hash == sha512Of("486951"));
926 	assert(user3.profilePicture == "new.png");
927 }
928 
929 unittest
930 {
931 	import vibe.db.mongo.mongo;
932 	import mongoschema.aliases : name, ignore, unique, binary;
933 	import std.digest.sha;
934 	import std.digest.md;
935 
936 	auto client = connectMongoDB("localhost");
937 
938 	struct Permission
939 	{
940 		string name;
941 		int priority;
942 	}
943 
944 	struct User
945 	{
946 		mixin MongoSchema;
947 
948 		@unique string username;
949 
950 		@binary()
951 		ubyte[] hash;
952 		@binary()
953 		ubyte[] salt;
954 
955 		@name("profile-picture")
956 		string profilePicture = "default.png";
957 
958 		Permission[] permissions;
959 
960 	@ignore:
961 		int sessionID;
962 	}
963 
964 	auto coll = client.getCollection("test.users2");
965 	coll.remove();
966 	coll.register!User;
967 
968 	User register(string name, string password)
969 	{
970 		User user;
971 		user.username = name;
972 		user.salt = md5Of(name).dup;
973 		user.hash = sha512Of(cast(ubyte[]) password ~ user.salt).dup;
974 		user.permissions ~= Permission("forum.access", 1);
975 		user.save();
976 		return user;
977 	}
978 
979 	User find(string name)
980 	{
981 		return User.findOne(["username" : name]);
982 	}
983 
984 	User a = register("foo", "bar");
985 	User b = find("foo");
986 	assert(a == b);
987 }