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 	static foreach (memberName; getSerializableMembers!obj)
228 	{
229 		{
230 			string name = memberName;
231 			static if (hasUDA!(__traits(getMember, obj, memberName), schemaName))
232 			{
233 				static assert(getUDAs!(__traits(getMember, obj, memberName), schemaName)
234 						.length == 1, "Member '" ~ memberName ~ "' can only have one name!");
235 				name = getUDAs!(__traits(getMember, obj, memberName), schemaName)[0].name;
236 			}
237 
238 			static if (!hasUDA!(__traits(getMember, obj, memberName), encode)
239 					&& !hasUDA!(__traits(getMember, obj, memberName), decode))
240 				ret ~= generateMember(memberName, name);
241 		}
242 	}
243 	return ret;
244 }
245 
246 struct Query(T)
247 {
248 	Bson[string] _query;
249 
250 	mixin(generateMembers!T(T.init));
251 
252 	static Bson toBson(Query!T query)
253 	{
254 		return Bson(query._query);
255 	}
256 }
257 
258 Query!T query(T)()
259 {
260 	return Query!T.init;
261 }
262 
263 Query!T and(T)(Query!T[] exprs...)
264 {
265 	return Query!T(["$and": memberToBson(exprs)]);
266 }
267 
268 Query!T not(T)(Query!T[] exprs)
269 {
270 	return Query!T(["$not": memberToBson(exprs)]);
271 }
272 
273 Query!T nor(T)(Query!T[] exprs...)
274 {
275 	return Query!T(["$nor": memberToBson(exprs)]);
276 }
277 
278 Query!T or(T)(Query!T[] exprs...)
279 {
280 	return Query!T(["$or": memberToBson(exprs)]);
281 }
282 
283 unittest
284 {
285 	struct CoolData
286 	{
287 		int number;
288 		bool boolean;
289 		string[] array;
290 		@schemaName("t")
291 		string text;
292 	}
293 
294 	assert(memberToBson(and(query!CoolData.number.gte(10),
295 			query!CoolData.number.lte(20), query!CoolData.boolean(true)
296 			.array.ofLength(10).text.regex("^yes"))).toString == Bson(
297 			[
298 				"$and": Bson([
299 					Bson(["number": Bson(["$gte": Bson(10)])]),
300 					Bson(["number": Bson(["$lte": Bson(20)])]),
301 					Bson([
302 						"array": Bson(["$size": Bson(10)]),
303 						"boolean": Bson(true),
304 						"t": Bson(["$regex": Bson("^yes")])
305 					]),
306 				])
307 			]).toString);
308 }