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 }