1 module selectors.where;
2 
3 import std.algorithm;
4 import std.array;
5 import std.traits;
6 
7 version(unittest) {
8   import fluent.asserts;
9 }
10 
11 ///
12 struct WhereAnyProxy(T: U[], U) {
13   private {
14     U[] list;
15   }
16 
17   alias M = U.M;
18   alias RootType = U.RootType;
19 
20   this(T list) {
21     this.list = list;
22   }
23 
24   ///
25   auto dispatch(string name, P...)() {
26     mixin(`alias R = ReturnType!(U.` ~ name ~ `);`);
27 
28     R[] result;
29 
30     foreach(localWhere; list) {
31       mixin(`result ~= localWhere.` ~ name ~ `();`);
32     }
33 
34     return whereAnyProxy(result);
35   }
36 
37   static if(isAggregateType!M) {
38     static foreach(member; __traits(allMembers, M)) {
39       static if(!std.traits.isArray!(typeof(__traits(getMember, M, member))) || isSomeString!(typeof(__traits(getMember, M, member))) ) {
40         mixin(`
41           auto ` ~ member ~ `(P...)() {
42             return dispatch!("` ~ member ~ `", P)();
43           }
44         `);
45       }
46     }
47   }
48 
49 
50   /// Does nothing but improve code readability
51   auto and() {
52     foreach(ref localWhere; list) {
53       localWhere = localWhere.and;
54     }
55 
56     return this;
57   }
58 
59   /// Negates the next filter
60   auto not() {
61     foreach(ref localWhere; list) {
62       localWhere = localWhere.not;
63     }
64 
65     return this;
66   }
67 
68   /// Check equality
69   auto equal(M value)() {
70     foreach(index, localWhere; list) {
71       list[index] = list[index].equal!value;
72     }
73 
74     return this;
75   }
76 
77   /// Returns all items that match at least one value
78   auto isAnyOf(T...)() {
79     foreach(ref localWhere; list) {
80       localWhere = localWhere.isAnyOf!T;
81     }
82 
83     return this;
84   }
85 
86   /// Check if the filtered list has at least one value
87   bool exists() {
88     return !list.filter!(a => a.exists).empty;
89   }
90 
91   /// Iterate over the filtered items
92   int opApply(scope int delegate(RootType) dg) {
93     int result = 0;
94 
95     foreach(item; list) {
96       if(item.exists) {
97         result = dg(item.rootItem);
98       }
99 
100       if (result)
101         break;
102     }
103     return result;
104   }
105 
106   /// Iterate over the filtered items
107   int opApply(scope int delegate(size_t index, RootType) dg) {
108     int result = 0;
109 
110     foreach(index, item; list) {
111       if(item.exists) {
112         result = dg(index, item.rootItem);
113       }
114 
115       if (result)
116         break;
117     }
118     return result;
119   }
120 }
121 
122 ///
123 auto whereAnyProxy(T)(T list) {
124   return WhereAnyProxy!(T)(list);
125 }
126 
127 ///
128 struct WhereAny(T : U[], string path, U) {
129   private {
130     U[] list;
131     bool negation;
132   }
133 
134   static if(path == "") {
135     alias M = U;
136   } else {
137     mixin(`alias M = typeof(U` ~ path ~ `);`);
138   }
139 
140   auto dispatch(string name)() {
141     mixin(`return whereAnyProxy(list.map!(item => where!(U, typeof(item.` ~ name ~ `))(item, item.` ~ name ~ `)).array);`);
142   }
143 
144   static if(isAggregateType!M) {
145     static foreach(member; __traits(allMembers, M)) {
146       static if(std.traits.isArray!(typeof(__traits(getMember, M, member))) && !isSomeString!(typeof(__traits(getMember, M, member))) ) {
147         mixin(`
148           auto ` ~ member ~ `() {
149             return dispatch!"` ~ member ~ `";
150           }
151         `);
152       }
153     }
154   }
155 }
156 
157 ///
158 struct Where(T : U[], string path, RootType_, U) {
159   private {
160     U[] list;
161     bool negation;
162   }
163 
164   alias RootType = RootType_;
165 
166   static if(path == "") {
167     alias M = U;
168   } else {
169     mixin(`alias M = typeof(U` ~ path ~ `);`);
170   }
171 
172   ///
173   this(U[] list) {
174     this.list = list;
175   }
176 
177   static if(!is(RootType == void)) {
178     private RootType _root;
179 
180     this(U[] list, RootType root) {
181       this.list = list;
182       this._root = root;
183     }
184 
185     ///
186     RootType rootItem() {
187       return _root;
188     }
189   }
190 
191   /// Filter by a member name
192   private auto dispatch(string member)() {
193     static if (path == "") {
194       enum newPath = "." ~ member;
195     } else {
196       enum newPath = path ~ "." ~ member;
197     }
198 
199     static assert(__traits(hasMember, M, member), "The `" ~ M.stringof ~ "` type has no `" ~ member ~ "` property.");
200 
201     static if(is(RootType == void)) {
202       return Where!(T, newPath, RootType)(list);
203     } else {
204       return Where!(T, newPath, RootType)(list, rootItem);
205     }
206   }
207 
208   static if(isAggregateType!M) {
209     static foreach(member; __traits(allMembers, M)) {
210       static if(!std.traits.isArray!(typeof(__traits(getMember, M, member))) || isSomeString!(typeof(__traits(getMember, M, member))) ) {
211         mixin(`
212           auto ` ~ member ~ `() {
213             return dispatch!"` ~ member ~ `";
214           }
215         `);
216       }
217     }
218   }
219 
220   /// Query array properties
221   auto any() {
222     return WhereAny!(T, path)(list);
223   }
224 
225   /// Does nothing but improve code readability
226   auto and() {
227     return this;
228   }
229 
230   /// Negates the next filter
231   auto not() {
232     negation = !negation;
233     return this;
234   }
235 
236   /// Check equality
237   auto equal(M value)() {
238     bool aff(const U item) pure {
239       mixin(`return item` ~ path ~ ` == value;`);
240     }
241 
242     bool neg(const U item) pure {
243       mixin(`return item` ~ path ~ ` != value;`);
244     }
245 
246     if(negation) {
247       this.list = list.filter!neg.array.dup;
248     } else {
249       this.list = list.filter!aff.array.dup;
250     }
251 
252     alias TT = typeof(this);
253     static if(is(RootType == void)) {
254       return TT(this.list);
255     } else {
256       return TT(this.list, this.rootItem);
257     }
258   }
259 
260   /// Returns all items that match at least one value
261   auto isAnyOf(T...)() {
262     string[] names;
263     static foreach(t; T) {
264       names ~= t;
265     }
266 
267     bool aff(const U item) pure {
268       mixin(`return names.canFind(item` ~ path ~ `);`);
269     }
270 
271     bool neg(const U item) pure {
272       mixin(`return !names.canFind(item` ~ path ~ `);`);
273     }
274 
275     if(negation) {
276       list = list.filter!neg.array.dup;
277     } else {
278       list = list.filter!aff.array.dup;
279     }
280 
281     alias TT = typeof(this);
282     static if(is(RootType == void)) {
283       return TT(this.list);
284     } else {
285       return TT(this.list, this.rootItem);
286     }
287   }
288 
289   /// Check if the filtered list has at least one value
290   bool exists() {
291     return list.length > 0;
292   }
293 
294   /// Iterate over the filtered items
295   int opApply(scope int delegate(ref U) dg) {
296     int result = 0;
297 
298     foreach(item; list) {
299       result = dg(item);
300 
301       if (result)
302         break;
303     }
304     return result;
305   }
306 
307   /// Iterate over the filtered items
308   int opApply(scope int delegate(size_t index, ref U) dg) {
309     int result = 0;
310 
311     foreach(index, item; list) {
312       result = dg(index, item);
313 
314       if (result)
315         break;
316     }
317     return result;
318   }
319 }
320 
321 /// Filter callables by attribute name
322 unittest {
323   import introspection.callable;
324   import introspection.attribute;
325 
326   @("test")
327   void test() { }
328 
329   enum item = describeCallable!test;
330   enum items = [ item ];
331 
332   ///auto hasAttribute = items.where.any.attributes.name.equal!`"test"`.exists;
333 
334   //hasAttribute.should.equal(true);
335   items.where.any.attributes.name.equal!"other".exists.should.equal(false);
336   auto tmp = items.where.any.attributes.name.isAnyOf!(["other", "attributes"]);
337 
338   tmp.exists.should.equal(false);
339 }
340 
341 version(unittest) { struct TestStructure { } }
342 
343 /// Filter callables by type name
344 unittest {
345   import introspection.aggregate;
346 
347   enum item = describeAggregate!TestStructure;
348   enum items = [ item ];
349 
350   items.where.type.name.equal!"TestStructure".and.exists.should.equal(true);
351   items.where.type.fullyQualifiedName.equal!"selectors.where.TestStructure".and.exists.should.equal(true);
352   items.where.type.fullyQualifiedName.equal!"selectors.where.OtherStructure".and.exists.should.equal(false);
353 
354   items.where.type.name.isAnyOf!"TestStructure".and.exists.should.equal(true);
355   items.where.type.fullyQualifiedName.isAnyOf!"selectors.where.TestStructure".and.exists.should.equal(true);
356   items.where.type.fullyQualifiedName.isAnyOf!"selectors.where.OtherStructure".and.exists.should.equal(false);
357 
358   items.where.type.name.not.equal!"TestStructure".and.exists.should.equal(false);
359   items.where.type.fullyQualifiedName.not.equal!"selectors.where.TestStructure".and.exists.should.equal(false);
360   items.where.type.fullyQualifiedName.not.equal!"selectors.where.OtherStructure".and.exists.should.equal(true);
361 
362   items.where.type.name.not.isAnyOf!"TestStructure".and.exists.should.equal(false);
363   items.where.type.fullyQualifiedName.not.isAnyOf!"selectors.where.TestStructure".and.exists.should.equal(false);
364   items.where.type.fullyQualifiedName.not.isAnyOf!"selectors.where.OtherStructure".and.exists.should.equal(true);
365 }
366 
367 /// Can iterate over filtered values
368 unittest {
369   import introspection.callable;
370 
371   @("test")
372   void foo() { }
373 
374   enum item = describeCallable!foo;
375   enum items = [ item ];
376 
377   /// iterate without index
378   size_t index;
379   foreach(element; items.where.any.attributes.name.equal!`"test"`) {
380     index.should.equal(0);
381     element.name.should.equal("foo");
382     index++;
383   }
384 
385   index = 0;
386   foreach(_; items.where.any.attributes.name.equal!`"other"`) {
387     index++;
388   }
389   index.should.equal(0);
390 
391   /// iterate with index
392   foreach(i, element; items.where.any.attributes.name.equal!`"test"`) {
393     i.should.equal(0);
394     element.name.should.equal("foo");
395   }
396 }
397 
398 /// query the introspection result
399 auto where(T)(T list) if(isArray!T){
400   return Where!(T, "", void)(list);
401 }
402 
403 /// ditto
404 auto where(T)(T item) if(!isArray!T){
405   return where([item]);
406 }
407 
408 /// ditto
409 auto where(T, U)(T rootItem, U list) if(isArray!U){
410   return Where!(U, "", T)(list, rootItem);
411 }
412 
413 /// ditto
414 auto where(T, U)(T rootItem, U item) if(!isArray!U){
415   return where(rootItem, [item]);
416 }