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 }