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