1 module dorm.types.patches;
2 
3 import dorm.api.db;
4 import dorm.declarative.conversion;
5 import dorm.model;
6 
7 import std.meta;
8 import std.traits;
9 
10 /**
11  * UDA to mark patch structs with, to make selecting them easier.
12  *
13  * Examples:
14  * ---
15  * @DormPatch!User
16  * struct UserSelection
17  * {
18  *     string username;
19  * }
20  * db.select!UserSelection;
21  *
22  * // is equivalent to
23  * struct UserSelection
24  * {
25  *     string username;
26  * }
27  * db.select!(User, UserSelection);
28  *
29  * // is equivalent to
30  * db.select!(User, "username");
31  *
32  * // is equivalent to
33  * db.select!(User, User.username);
34  *
35  * // is equivalent to
36  * db.select!(User, Tuple!(string, "username"));
37  * ---
38  */
39 struct DormPatch(User)
40 {
41 }
42 
43 /// Helper to validate if a DormPatch annotated field is valid for the given
44 /// Model type.
45 mixin template ValidatePatch(Patch, TModel)
46 {
47 	import std.traits : hasUDA;
48 	import dorm.annotations : isDormFieldAttribute;
49 
50 	static assert(is(TModel : Model), "Passed non-Model to ValidatePatch");
51 
52 	static if (!is(Patch == TModel) && !isImplicitPatch!(TModel, Patch))
53 	{
54 		static assert (hasUDA!(Patch, DormPatch!TModel), "Patch struct " ~ Patch.stringof
55 			~ " must be annoated using DormPatch!(" ~ TModel.stringof ~ ") exactly once!");
56 
57 		static foreach (i, field; Patch.tupleof)
58 		{
59 			static assert (__traits(hasMember, TModel.init, Patch.tupleof[i].stringof),
60 				"\n" ~ SourceLocation(__traits(getLocation, Patch.tupleof[i])).toErrorString
61 					~ "Patch field `" ~ Patch.tupleof[i].stringof
62 					~ "` is not defined on DB Type " ~ TModel.stringof
63 					~ ".\n\tAvailable usable fields: "
64 						~ DormFields!TModel.map!(f => f.sourceColumn).join(", "));
65 
66 			static foreach (attr; __traits(getAttributes, Patch.tupleof[i]))
67 				static assert (!isDormFieldAttribute!attr,
68 					"\n" ~ SourceLocation(__traits(getLocation, Patch.tupleof[i])).toErrorString
69 						~ "Patch field `" ~ Patch.tupleof[i].stringof
70 						~ "` defines DB-related annotations, which is not"
71 						~ " supported. Put annotations on the Model field instead!");
72 		}
73 	}
74 }
75 
76 /// Checks if Patch is an implicit patch of TModel. That is a child struct of
77 /// the given model class.
78 enum isImplicitPatch(TModel, Patch) = is(__traits(parent, Patch) == TModel);
79 
80 template ImplicitPatchFieldName(TModel, Patch)
81 {
82 	static assert(isImplicitPatch!(TModel, Patch));
83 
84 	static foreach (member; TModel.tupleof)
85 	{
86 		static if (is(typeof(member) == Patch))
87 		{
88 			enum ImplicitPatchFieldName = __traits(identifier, member);
89 		}
90 	}
91 }
92 
93 /// Checks if the given type Patch is either an implicit or an explicit patch.
94 enum isSomePatch(Patch) =
95 	is(Patch == struct) && (is(__traits(parent, Patch) : Model)
96 		|| hasUDA!(Patch, DormPatch));
97 enum isSomePatch(alias other) = false;
98 
99 static template PatchFromIdOrPatch(alias idOrPatch)
100 {
101 	static if (is(idOrPatch : Model))
102 		alias PatchFromIdOrPatch = idOrPatch;
103 	else static if (isSomePatch!idOrPatch)
104 		alias PatchFromIdOrPatch = idOrPatch;
105 	else static if (is(__traits(parent, idOrPatch) : Model)
106 		|| isSomePatch!(__traits(parent, idOrPatch)))
107 		alias PatchFromIdOrPatch = __traits(parent, idOrPatch);
108 	else
109 		static assert(false, "Invalid id or patch: " ~ idOrPatch.stringof);
110 }
111 
112 static template IdAliasFromIdOrPatch(alias idOrPatch)
113 {
114 	static if (is(idOrPatch : Model))
115 		alias IdAliasFromIdOrPatch =
116 			__traits(getMember, idOrPatch, DormPrimaryKeyName!idOrPatch);
117 	else static if (isSomePatch!idOrPatch)
118 		alias IdAliasFromIdOrPatch =
119 			__traits(getMember, idOrPatch,
120 				DormPrimaryKeyName!(ModelFromSomePatch!idOrPatch));
121 	else static if (is(__traits(parent, idOrPatch) : Model)
122 		|| isSomePatch!(__traits(parent, idOrPatch)))
123 		alias IdAliasFromIdOrPatch = idOrPatch;
124 	else
125 		static assert(false, "Invalid id or patch: " ~ idOrPatch.stringof);
126 }
127 
128 static template ModelFromIdOrModel(alias idOrModel)
129 {
130 	static if (is(idOrModel : Model))
131 		alias ModelFromIdOrModel = idOrModel;
132 	else static if (is(__traits(parent, idOrModel) : Model))
133 		alias ModelFromIdOrModel = __traits(parent, idOrModel);
134 	else
135 		static assert(false, "Invalid id or model: " ~ idOrModel.stringof);
136 }
137 
138 static template IdAliasFromIdOrModel(alias idOrModel)
139 {
140 	static if (is(idOrModel : Model))
141 		alias IdAliasFromIdOrModel =
142 			__traits(getMember, idOrModel, DormPrimaryKeyName!idOrModel);
143 	else static if (is(__traits(parent, idOrModel) : Model))
144 		alias IdAliasFromIdOrModel = idOrModel;
145 	else
146 		static assert(false, "Invalid id or model: " ~ idOrModel.stringof);
147 }
148 
149 static template ModelFromSomePatch(TModel)
150 {
151 	static if (is(__traits(parent, TModel) : Model))
152 		alias ModelFromSomePatch = __traits(parent, TModel);
153 	else
154 		alias ModelFromSomePatch = DBType!TModel;
155 }
156 
157 static template TypeOfField(TModel, alias field)
158 {
159 	static if (is(__traits(parent, field) == TModel)
160 		|| is(__traits(parent, __traits(parent, field)) == TModel))
161 		alias TypeOfField = typeof(field);
162 	else
163 		static assert(false,
164 			"Can't use field " ~ field.stringof ~ " on Model type " ~ TModel.stringof);
165 }
166 
167 template DBType(Selection...)
168 {
169 	static assert(Selection.length >= 1);
170 
171 	static if (Selection.length > 1)
172 	{
173 		alias DBType = GetModelDBType!(Selection[0]);
174 	}
175 	else
176 	{
177 		alias PatchAttrs = getUDAs!(Selection[0], DormPatch);
178 		static if (PatchAttrs.length == 0)
179 			alias DBType = GetModelDBType!(Selection[0]);
180 		else static if (PatchAttrs.length == 1)
181 		{
182 			static if (is(PatchAttrs[0] == DormPatch!T, T))
183 			{
184 				mixin ValidatePatch!(Selection[0], T);
185 				alias DBType = T;
186 			}
187 			else
188 				static assert(false, "internal template error");
189 		}
190 		else
191 			static assert(false, "Cannot annotate DormPatch struct with multiple DormPatch UDAs.");
192 	}
193 }
194 
195 template GetModelDBType(T)
196 {
197 	import dorm.model : Model;
198 
199 	static if (is(T : Model))
200 		alias GetModelDBType = T;
201 	else static if (is(__traits(parent, T) : Model))
202 		alias GetModelDBType = __traits(parent, T);
203 	else
204 		static assert(false, "Passed in Non-Model Type where a Model was expected");
205 }
206 
207 template SelectType(T, Selection...)
208 {
209 	import std.traits : isAggregateType;
210 
211 	static if (Selection.length == 0)
212 		alias SelectType = T;
213 	else static if (Selection.length == 1 && isAggregateType!(Selection[0]))
214 		alias SelectType = Selection[0];
215 	else
216 		alias SelectType = BuildFieldsTuple!(T, Selection);
217 }
218 
219 template BuildFieldsTuple(T, Selection...)
220 {
221 	import std.meta : AliasSeq;
222 	import std.typecons : Tuple;
223 
224 	alias TupleArgs = AliasSeq!();
225 	static foreach (alias Field; Selection)
226 	{
227 		static if (__traits(compiles, { string s = Field; }))
228 			alias TupleArgs = AliasSeq!(TupleArgs, typeof(__traits(getMember, T, Field)), Field);
229 		else
230 			alias TupleArgs = AliasSeq!(TupleArgs, typeof(Field), __traits(identifier, Field));
231 	}
232 	alias BuildFieldsTuple = Tuple!TupleArgs;
233 }
234 
235 template FilterLayoutFields(T, TSelect)
236 {
237 	static if (is(T == TSelect))
238 		enum FilterLayoutFields = DormFields!T;
239 	else static if (is(TSelect : Model))
240 		static assert(false, "Cannot filter for fields of Model class on a Model class");
241 	else static if (isImplicitPatch!(T, TSelect))
242 		enum FilterLayoutFields = filterFieldsPrefixed!T(
243 			ImplicitPatchFieldName!(T, TSelect) ~ ".",
244 			implicitPatchSelectionFieldNames!(T, TSelect)
245 		);
246 	else
247 		enum FilterLayoutFields = filterFields!T(selectionFieldNames!(T, TSelect));
248 }
249 
250 private auto filterFields(T)(string[] sourceNames...)
251 {
252 	import std.algorithm : canFind;
253 
254 	enum fields = DormFields!T;
255 	typeof(fields) ret;
256 	foreach (ref field; fields)
257 		if (sourceNames.canFind(field.sourceColumn))
258 			ret ~= field;
259 	return ret;
260 }
261 
262 private auto filterFieldsPrefixed(T)(string prefix, string[] sourceNames...)
263 {
264 	import std.algorithm : canFind, startsWith;
265 
266 	enum fields = DormFields!T;
267 	typeof(fields) ret;
268 	foreach (ref field; fields)
269 		if (field.sourceColumn.startsWith(prefix)
270 			&& sourceNames.canFind(field.sourceColumn[prefix.length .. $]))
271 		{
272 			field.sourceColumn = field.sourceColumn[prefix.length .. $];
273 			ret ~= field;
274 		}
275 	return ret;
276 }
277 
278 private string[] implicitPatchSelectionFieldNames(T, TSelect)()
279 {
280 	static foreach (field; __traits(allMembers, T))
281 		static if (field == ImplicitPatchFieldName!(T, TSelect))
282 			return selectionFieldNames!(T, TSelect)();
283 }
284 
285 private string[] selectionFieldNames(T, TSelect)(string prefix = "")
286 {
287 	import std.algorithm : canFind;
288 
289 	enum layout = DormLayout!T;
290 
291 	string[] ret;
292 	static foreach (field; __traits(allMembers, TSelect))
293 	{
294 		static if (layout.embeddedStructs.canFind(field))
295 			ret ~= selectionFieldNames!(T, typeof(__traits(getMember, TSelect, field)))(
296 				prefix ~ field ~ ".");
297 		else
298 			ret ~= (prefix ~ field);
299 	}
300 	return ret;
301 }