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 }