1 /// This module provides a typesafe querying framework.
2 /// For now only very basic queries are supported
3 module mongoschema.query;
4 
5 import mongoschema;
6 
7 import std.regex;
8 import std.traits;
9 
10 /// Represents a field to compare
11 struct FieldQuery(T, Obj)
12 {
13 	enum isCompatible(V) = is(V : T) || is(V == Bson);
14 
15 	Query!Obj* query;
16 	string name;
17 
18 	@disable this();
19 	@disable this(this);
20 
21 	private this(string name, ref Query!Obj query) @trusted
22 	{
23 		this.name = name;
24 		this.query = &query;
25 	}
26 
27 	ref Query!Obj equals(V)(V other) if (isCompatible!V)
28 	{
29 		query._query[name] = memberToBson(other);
30 		return *query;
31 	}
32 
33 	alias equal = equals;
34 	alias eq = equals;
35 
36 	ref Query!Obj ne(V)(V other) if (isCompatible!V)
37 	{
38 		query._query[name] = Bson(["$ne": memberToBson(other)]);
39 		return *query;
40 	}
41 
42 	alias notEqual = ne;
43 	alias notEquals = ne;
44 
45 	ref Query!Obj gt(V)(V other) if (isCompatible!V)
46 	{
47 		query._query[name] = Bson(["$gt": memberToBson(other)]);
48 		return *query;
49 	}
50 
51 	alias greaterThan = gt;
52 
53 	ref Query!Obj gte(V)(V other) if (isCompatible!V)
54 	{
55 		query._query[name] = Bson(["$gte": memberToBson(other)]);
56 		return *query;
57 	}
58 
59 	alias greaterThanOrEqual = gt;
60 
61 	ref Query!Obj lt(V)(V other) if (isCompatible!V)
62 	{
63 		query._query[name] = Bson(["$lt": memberToBson(other)]);
64 		return *query;
65 	}
66 
67 	alias lessThan = lt;
68 
69 	ref Query!Obj lte(V)(V other) if (isCompatible!V)
70 	{
71 		query._query[name] = Bson(["$lte": memberToBson(other)]);
72 		return *query;
73 	}
74 
75 	alias lessThanOrEqual = lt;
76 
77 	ref Query!Obj oneOf(Args...)(Args other)
78 	{
79 		Bson[] arr = new Bson(Args.length);
80 		static foreach (i, arg; other)
81 			arr[i] = memberToBson(arg);
82 		query._query[name] = Bson(["$in": Bson(arr)]);
83 		return *query;
84 	}
85 
86 	ref Query!Obj inArray(V)(V[] array) if (isCompatible!V)
87 	{
88 		query._query[name] = Bson(["$in": memberToBson(array)]);
89 		return *query;
90 	}
91 
92 	ref Query!Obj noneOf(Args...)(Args other)
93 	{
94 		Bson[] arr = new Bson(Args.length);
95 		static foreach (i, arg; other)
96 			arr[i] = memberToBson(arg);
97 		query._query[name] = Bson(["$nin": Bson(arr)]);
98 		return *query;
99 	}
100 
101 	alias notOneOf = noneOf;
102 
103 	ref Query!Obj notInArray(V)(V[] array) if (isCompatible!V)
104 	{
105 		query._query[name] = Bson(["$nin": memberToBson(array)]);
106 		return *query;
107 	}
108 
109 	ref Query!Obj exists(bool exists = true)
110 	{
111 		query._query[name] = Bson(["$exists": Bson(exists)]);
112 		return *query;
113 	}
114 
115 	ref Query!Obj typeOf(Bson.Type type)
116 	{
117 		query._query[name] = Bson(["$type": Bson(cast(int) type)]);
118 		return *query;
119 	}
120 
121 	ref Query!Obj typeOfAny(Bson.Type[] types...)
122 	{
123 		Bson[] arr = new Bson[types.length];
124 		foreach (i, type; types)
125 			arr[i] = Bson(cast(int) type);
126 		query._query[name] = Bson(["$type": Bson(arr)]);
127 		return *query;
128 	}
129 
130 	ref Query!Obj typeOfAny(Bson.Type[] types)
131 	{
132 		query._query[name] = Bson(["$type": serializeToBson(types)]);
133 		return *query;
134 	}
135 
136 	static if (is(T : U[], U))
137 	{
138 		ref Query!Obj containsAll(U[] values)
139 		{
140 			query._query[name] = Bson(["$all": serializeToBson(values)]);
141 			return *query;
142 		}
143 
144 		alias all = containsAll;
145 
146 		ref Query!Obj ofLength(size_t length)
147 		{
148 			query._query[name] = Bson(["$size": Bson(length)]);
149 			return *query;
150 		}
151 
152 		alias size = ofLength;
153 	}
154 
155 	static if (isIntegral!T)
156 	{
157 		ref Query!Obj bitsAllClear(T other)
158 		{
159 			query._query[name] = Bson(["$bitsAllClear": Bson(other)]);
160 			return *query;
161 		}
162 
163 		ref Query!Obj bitsAllSet(T other)
164 		{
165 			query._query[name] = Bson(["$bitsAllSet": Bson(other)]);
166 			return *query;
167 		}
168 
169 		ref Query!Obj bitsAnyClear(T other)
170 		{
171 			query._query[name] = Bson(["$bitsAnyClear": Bson(other)]);
172 			return *query;
173 		}
174 
175 		ref Query!Obj bitsAnySet(T other)
176 		{
177 			query._query[name] = Bson(["$bitsAnySet": Bson(other)]);
178 			return *query;
179 		}
180 	}
181 
182 	static if (isNumeric!T)
183 	{
184 		ref Query!Obj remainder(T divisor, T remainder)
185 		{
186 			query._query[name] = Bson(["$mod": Bson([Bson(divisor), Bson(remainder)])]);
187 			return *query;
188 		}
189 	}
190 
191 	static if (isSomeString!T)
192 	{
193 		ref Query!Obj regex(string regex, string options = null)
194 		{
195 			if (options.length)
196 				query._query[name] = Bson([
197 						"$regex": Bson(regex),
198 						"$options": Bson(options)
199 						]);
200 			else
201 				query._query[name] = Bson(["$regex": Bson(regex)]);
202 			return *query;
203 		}
204 	}
205 }
206 
207 private string generateMember(string member, string name)
208 {
209 	return `alias T_` ~ member ~ ` = typeof(__traits(getMember, T.init, "` ~ member ~ `"));
210 
211 	FieldQuery!(T_` ~ member
212 		~ `, T) ` ~ member ~ `()
213 	{
214 		return FieldQuery!(T_` ~ member ~ `, T)(` ~ '`' ~ name ~ '`' ~ `, this);
215 	}
216 
217 	typeof(this) ` ~ member
218 		~ `(T_` ~ member ~ ` equals)
219 	{
220 		return ` ~ member ~ `.equals(equals);
221 	}`;
222 }
223 
224 private string generateMembers(T)(T obj)
225 {
226 	string ret;
227 	foreach (memberName; __traits(allMembers, T))
228 	{
229 		static if (memberName == "_schema_object_id_")
230 			continue;
231 		else static if (__traits(compiles, {
232 				static s = isVariable!(__traits(getMember, obj, memberName));
233 			}) && isVariable!(__traits(getMember, obj, memberName)) && !__traits(compiles, {
234 				static s = __traits(getMember, T, memberName);
235 			}) // No static members
236 			 && __traits(compiles, {
237 				typeof(__traits(getMember, obj, memberName)) t = __traits(getMember, obj, memberName);
238 			}))
239 		{
240 			static if (__traits(getProtection, __traits(getMember, obj, memberName)) == "public")
241 			{
242 				string name = memberName;
243 				static if (!hasUDA!((__traits(getMember, obj, memberName)), schemaIgnore))
244 				{
245 					static if (hasUDA!((__traits(getMember, obj, memberName)), schemaName))
246 					{
247 						static assert(getUDAs!((__traits(getMember, obj, memberName)), schemaName)
248 								.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
249 						name = getUDAs!((__traits(getMember, obj, memberName)), schemaName)[0].name;
250 					}
251 					ret ~= generateMember(memberName, name);
252 				}
253 			}
254 		}
255 	}
256 	return ret;
257 }
258 
259 struct Query(T)
260 {
261 	Bson[string] _query;
262 
263 	mixin(generateMembers!T(T.init));
264 
265 	static Bson toBson(Query!T query)
266 	{
267 		return Bson(query._query);
268 	}
269 }
270 
271 Query!T query(T)()
272 {
273 	return Query!T.init;
274 }
275 
276 Query!T and(T)(Query!T[] exprs...)
277 {
278 	return Query!T(["$and": memberToBson(exprs)]);
279 }
280 
281 Query!T not(T)(Query!T[] exprs)
282 {
283 	return Query!T(["$not": memberToBson(exprs)]);
284 }
285 
286 Query!T nor(T)(Query!T[] exprs...)
287 {
288 	return Query!T(["$nor": memberToBson(exprs)]);
289 }
290 
291 Query!T or(T)(Query!T[] exprs...)
292 {
293 	return Query!T(["$or": memberToBson(exprs)]);
294 }
295 
296 unittest
297 {
298 	struct CoolData
299 	{
300 		int number;
301 		bool boolean;
302 		string[] array;
303 		@schemaName("t")
304 		string text;
305 	}
306 
307 	assert(memberToBson(and(query!CoolData.number.gte(10),
308 			query!CoolData.number.lte(20), query!CoolData.boolean(true)
309 			.array.ofLength(10).text.regex("^yes"))).toString == Bson(
310 			[
311 				"$and": Bson([
312 					Bson(["number": Bson(["$gte": Bson(10)])]),
313 					Bson(["number": Bson(["$lte": Bson(20)])]),
314 					Bson([
315 						"array": Bson(["$size": Bson(10)]),
316 						"boolean": Bson(true),
317 						"t": Bson(["$regex": Bson("^yes")])
318 					]),
319 				])
320 			]).toString);
321 }