1 module scorpion.repository;
2 
3 import std.algorithm : map;
4 import std.conv : to;
5 import std..string : startsWith, join;
6 import std.traits : ReturnType, Parameters, ParameterIdentifierTuple, hasUDA, getUDAs;
7 
8 import shark : Database, PrimaryKey;
9 
10 import scorpion.entity : Entity, ExtendEntity;
11 
12 /**
13  * Base interface for repositories. Every repository that implements
14  * this interface will be extended by scorpion and its methods implemented.
15  * Every method in the interface must have either the attribute @Select,
16  * @Insert, @Update or @Remove that indicates which action the repository
17  * will perform using the database.
18  * Example:
19  * ---
20  * @Entity("example")
21  * class Example {
22  * 
23  *    @PrimaryKey
24  *    Integer a;
25  * 
26  *    String b;
27  * 
28  *    Time c;
29  * 
30  * }
31  * 
32  * interface ExampleRepository : Repository!Example {
33  * 
34  *    @Select
35  *    Example[] selectAll();
36  * 
37  *    @Insert
38  *    void insert(Example);
39  * 
40  *    @Update
41  *    void update(Example);
42  * 
43  *    @Remove
44  *    void remove(Example);
45  * 
46  * }
47  * ---
48  */
49 interface Repository(T) {}
50 
51 /**
52  * Indicates that one or more entities will be selected from
53  * the database. The return type of a method marked with the
54  * @Select atribute can be either T or T[], where T is the entity
55  * specified in the repository's declaration.
56  * Example:
57  * ---
58  * interface ExampleRepository : Repository!Example {
59  * 
60  *    @Select
61  *    T[] selectAll();
62  * 
63  *    @Select
64  *    T selectOne(Integer id);
65  * 
66  * }
67  * ---
68  */
69 enum Select;
70 
71 /**
72  * Indicates that a newly created entity will be inserted in
73  * the database. If the entity has one or more primary keys
74  * their values will be updated.
75  * Example:
76  * ---
77  * interface ExampleRepository : Repository!Example {
78  * 
79  *    @Insert
80  *    void insert(Example entity);
81  * 
82  * }
83  * ---
84  */
85 enum Insert;
86 
87 /**
88  * Indicates that an existing entity will be updated.
89  * The updated fields can be specified with the `Fields`
90  * attribute; if the attributes is not present all fields
91  * except the primary keys will be updated.
92  * If the entity does not have any primary key the `Where`
93  * attribute must also be present.
94  * Example:
95  * ---
96  * interface ExampleRepository : Repository!Example {
97  * 
98  *    @Update
99  *    void update(Example entity);
100  * 
101  * }
102  * ---
103  */
104 enum Update;
105 
106 /**
107  * Deletes one or more entities from the database.
108  * If the entity does not have any primary key the
109  * `Where` attribute must also be present.
110  * Example:
111  * ---
112  * interface ExampleRepository : Repository!Example {
113  * 
114  *    @Remove
115  *    void remove(Example entity);
116  * 
117  * }
118  * ---
119  * The name of the attribute id `Remove` and not
120  * `Delete` due to a conflict with the `Delete` HTTP
121  * method name and the fact that `delete` is a reserved
122  * keyword in D, thus methods could not be called simply
123  * `delete`.
124  */
125 enum Remove;
126 
127 /**
128  * Adds a `where` clause to the query. This attribute can be
129  * used with query that select, update and remove entities.
130  * It is possible to use method's arguments in the `where`
131  * clause by adding the dollar sign plus the index (starting
132  * from 0, obviously) of the variable.
133  * Example:
134  * ---
135  * interface ExampleRepository : Repository!Example {
136  * 
137  *    @Select
138  *    @Where("b=$0")
139  *    Example[] selectByB(string b);
140  * 
141  *    @Update
142  *    @Where("b=$0 or b=$1")
143  *    void update(string b0, string b1);
144  * 
145  * }
146  * ---
147  */
148 struct Where { string clause; }
149 
150 /**
151  * Specifies in which order the result of a select query
152  * should be returned.
153  * It is possible to add one or more `OrderBy` attributes
154  * to the same method with different field names.
155  * Example:
156  * ---
157  * interface ExampleRepository : Repository!Example {
158  * 
159  *    @OrderBy("b")
160  *    @OrderBy("a", OrderBy.desc)
161  *    Example[] select();
162  * 
163  * }
164  * ---
165  */
166 alias OrderBy = Database.Clause.Order.Field;
167 
168 /**
169  * Specifies that the result of a select query should be
170  * returned randomly ordered. This action is usually performed
171  * directly from the database.
172  * It's not possible to use any other order attribute associated
173  * with this one.
174  */
175 enum OrderByRandom;
176 
177 /**
178  * Limits the number of results returned by a select query.
179  * It is possible, like in the `Where` attribute, to specify
180  * a value using the arguments by adding the dollar sign and
181  * the argument's index in the method.
182  * Example:
183  * ---
184  * interface ExampleRepository : Repository!Example {
185  * 
186  *    @Limit(5)
187  *    Example[] select5();
188  * 
189  *    @Limit("$0", "$1")
190  *    Example[] selectInRange(size_t lowerLimit, size_t upperLimit);
191  * 
192  * }
193  * ---
194  */
195 struct Limit {
196 
197 	string lower, upper;
198 
199 	this(L, U)(L lower, U upper) if(__traits(compiles, to!string(lower)) && __traits(compiles, to!string(upper))) {
200 		this.lower = lower.to!string;
201 		this.upper = upper.to!string;
202 	}
203 
204 	this(U)(U upper) if(__traits(compiles, to!string(upper))) {
205 		this("0", upper);
206 	}
207 
208 }
209 
210 /**
211  * Indicates which field(s) to update when updating an entity.
212  * This is useful when having an entity with a field that holds
213  * big data and and a small field that is updated frequently.
214  * By adding the `Fields` attribute the big data is not sent to
215  * the database and updated every time the small field is, saving
216  * time.
217  * Example:
218  * ---
219  * interface ExampleRepository : Repository!Example {
220  * 
221  *    @Update
222  *    @Fields("b")
223  *    void updateB(Example entity);
224  * 
225  *    @Update
226  *    @Fields("c")
227  *    void updateC(Example entity);
228  * 
229  * }
230  * ---
231  */
232 struct Fields {
233 
234 	string[] fields;
235 
236 	this(string[] fields...) {
237 		this.fields = fields;
238 	}
239 
240 }
241 
242 class DatabaseRepository(T:Repository!R, R) : T {
243 
244 	private enum __table = getUDAs!(R, Entity)[0].name;
245 	private alias E = ExtendEntity!(R, __table);
246 
247 	private Database _database;
248 
249 	public this(Database database) {
250 		_database = database;
251 	}
252 
253 	mixin(extendInterface!(T, R));
254 
255 }
256 
257 private string extendInterface(T, E)() {
258 	string ret = "";
259 	foreach(i, immutable member; __traits(allMembers, T)) {
260 		alias M = __traits(getMember, T, member);
261 		alias R = ReturnType!M;
262 		ret ~= "override ReturnType!(__traits(getMember, T, `" ~ member ~ "`)) " ~ member ~ "(Parameters!(__traits(getMember, T, `" ~ member ~ "`)) args){";
263 		static if(hasUDA!(M, Where)) {
264 			enum where = true;
265 			ret ~= "auto where = Database.Clause.Where.prepare!(E, `" ~ getUDAs!(M, Where)[0].clause ~ "`).build(args);";
266 		} else {
267 			enum where = false;
268 			ret ~= "enum where = Database.Clause.Where.init;";
269 		}
270 		static if(hasUDA!(M, OrderByRandom)) {
271 			ret ~= "enum order = Database.Clause.Order.random;";
272 		} else static if(hasUDA!(M, OrderBy)) {
273 			ret ~= "enum order = Database.Clause.Order(";
274 			foreach(order ; getUDAs!(M, OrderBy)) {
275 				//TODO convert member name
276 				ret ~= "Database.Clause.Order.Field(`" ~ order.name ~ "`, Database.Clause.Order.Field." ~ (order._asc ? "asc" : "desc") ~ "),";
277 			}
278 			ret ~= ");";
279 		} else {
280 			ret ~= "enum order = Database.Clause.Order.init;";
281 		}
282 		static if(hasUDA!(M, Limit)) {
283 			immutable limit = getUDAs!(__traits(getMember, T, member), Limit)[0];
284 			ret ~= "auto limit = Database.Clause.Limit(" ~ convert(limit.lower) ~ "," ~ convert(limit.upper) ~ ");";
285 		} else {
286 			ret ~= "enum limit = Database.Clause.Limit.init;";
287 		}
288 		static if(hasUDA!(M, Select)) {
289 			static if(is(R == E)) {
290 				ret ~= "return _database.selectOne!E(Database.Select(where, order, limit));";
291 			} else {
292 				ret ~= "R[] ret; foreach(entity ; _database.select!E(Database.Select(where, order, limit))){ ret ~= entity; } return ret;";
293 			}
294 		} else static if(hasUDA!(M, Insert)) {
295 			static assert(is(R == void));
296 			ret ~= "_database.insert(new E(args[0]));";
297 		} else static if(hasUDA!(M, Update)) {
298 			static if(hasUDA!(M, Fields)) enum fields = getUDAs!(M, Fields).fields;
299 			else enum fields = members!E;
300 			static if(where) {
301 				ret ~= "_database.update!([" ~ fields.map!(str => '"' ~ str ~ '"').join(",") ~ "])(new E(args[0]), where);";
302 			} else {
303 				ret ~= "_database.update!([" ~ fields.map!(str => '"' ~ str ~ '"').join(",") ~ "])(new E(args[0]));";
304 			}
305 		} else static if(hasUDA!(M, Remove)) {
306 			static if(Parameters!M.length == 1 && is(Parameters!M[0] == E)) {
307 				ret ~= "_database.del(new E(args[0]));";
308 			} else {
309 				ret ~= "_database.del(__table, where);";
310 			}
311 		} else {
312 			static assert(0, "Cannot implement method " ~ member ~ " because it's missing either @Select, @Insert, @Update or @Remove");
313 		}
314 		ret ~= "}";
315 	}
316 	return ret;
317 }
318 
319 private string convert(string str) {
320 	if(str.length && str[0] == '$') return "args[" ~ str[1..$] ~ "]";
321 	else return str;
322 }
323 
324 // copied from shark.database.getEntityMembers
325 private string[] members(T)() {
326 	string[] ret;
327 	foreach(immutable member ; __traits(allMembers, T)) {
328 		static if(!is(typeof(__traits(getMember, T, member)) == function) && __traits(compiles, mixin("new T()." ~ member ~ "=T." ~ member ~ ".init")) && !hasUDA!(__traits(getMember, T, member), PrimaryKey)) {
329 			ret ~= member;
330 		}
331 	}
332 	return ret;
333 }