1 ///
2 module dpq.attributes;
3 
4 import std.traits;
5 import std.typetuple;
6 import std.typecons;
7 
8 import dpq.column;
9 import dpq.meta;
10 
11 version(unittest) import std.stdio;
12 
13 /**
14    @relation attribute -- specifies the relation/type name to be used
15    for Connection's ORM functions. If none is set, dpq will
16    default to the structure's name in lower_snake_case.
17  */
18 RelationAttribute relation(string name)
19 {
20    return RelationAttribute(name);
21 }
22 
23 package struct RelationAttribute
24 {
25    string name;
26 }
27 
28 enum EmbedAttribute;
29 deprecated("There is no need to use @embed anymore, Serialisers should take care of it")
30    alias embed = EmbedAttribute;
31 
32 /**
33    @attribute / @attr -- specifies the name of the attribute to be used.
34 
35    Defaults to member's name in lower_snake_case if nothing is specified
36  */
37 AttributeAttribute attribute(string name)
38 {
39    return AttributeAttribute(name);
40 }
41 alias attr = attribute;
42 
43 package struct AttributeAttribute
44 {
45    string name;
46 }
47 
48 /**
49    @PK / @Pkey / @PrimaryKey attribute -- specifies that the member should be
50    a PK. Coumpound PKs are not supported.
51  */
52 package enum PrimaryKeyAttribute;
53 
54 alias PrimaryKey = PrimaryKeyAttribute;
55 alias PKey = PrimaryKey;
56 alias PK = PrimaryKey;
57 
58 
59 
60 /**
61    Specifies the type for the member, overrides any serialiser-provided type
62  */
63 PGTypeAttribute type(string type)
64 {
65    return PGTypeAttribute(type);
66 }
67 
68 /// Shortcut for @type("SERIAL")
69 @property PGTypeAttribute serial()
70 {
71    return PGTypeAttribute("SERIAL");
72 }
73 
74 /// SHORTCUT for @type("SERIAL4")
75 @property PGTypeAttribute serial4()
76 {
77    return PGTypeAttribute("SERIAL4");
78 }
79 
80 /// SHORTCUT for @type("SERIAL8")
81 @property PGTypeAttribute serial8()
82 {
83    return PGTypeAttribute("SERIAL8");
84 }
85 
86 package struct PGTypeAttribute
87 {
88    string type;
89 }
90 
91 /**
92    Allows placing any text after the column definition when ensureSchema is ran.
93 
94    Useful for stuff like "NOT NULL", or "CHECK (x > y)", ...
95 
96    Do not use to create foreign keys, use @FK instead.
97 
98    Example:
99    -------------
100    @relation("testy")
101    struct Test
102    {
103       @serial @PK int id;
104       @suffix("NOT NULL") string username; // will produce "username TEXT NOT NULL"
105    }
106    -------------
107  */
108 ColumnSuffixAttribute suffix(string suffix)
109 {
110    return ColumnSuffixAttribute(suffix);
111 }
112 
113 struct ColumnSuffixAttribute
114 {
115    string suffix;
116 }
117 
118 /**
119    A shortcut to @suffix("NOT NULL")
120 
121    Does not perform any kind of a null check in D.
122  */
123 enum notNull = suffix("NOT NULL");
124 
125 /**
126    Specifies that the member should be completely ignored as far as the DB is
127    concerned.
128  */
129 package enum IgnoreAttribute;
130 alias ignore = IgnoreAttribute;
131 
132 /**
133    Specifies that the member/column should have an index created on it.
134    If unique is set, it well be a unique index.
135  */
136 package struct IndexAttribute
137 {
138    bool unique = false;
139 }
140 
141 /// @index
142 @property IndexAttribute index()
143 {
144    return IndexAttribute();
145 }
146 
147 /// @uniqueIndex
148 @property IndexAttribute uniqueIndex()
149 {
150    return IndexAttribute(true);
151 }
152 
153 /**
154    Specifies that the member is a foriegn key, ensureSchema will create a FK
155    constraint as well as an index for it. Finds the referenced table's PK
156    by itself.
157  */
158 package struct ForeignKeyAttribute
159 {
160    string relation;
161    string pkey;
162 }
163 
164 @property ForeignKeyAttribute foreignKey(T)()
165 {
166    return ForeignKeyAttribute(relationName!T, primaryKeyName!T);
167 }
168 alias FK = foreignKey;
169 alias FKey = foreignKey;
170 
171 /**
172    Transforms the given string into lower_snake_case at compile-time.
173    Used for attribute and relation names and probably not very useful
174    outside the library itself.
175  */
176 template SnakeCase(string str)
177 {
178    import std.string : toLower;
179 
180    template IsLower(char c)
181    {
182       enum IsLower = (c >= 'a' && c <= 'z');
183    }
184    template IsUpper(char c)
185    {
186       enum IsUpper = (c >= 'A' && c <= 'Z');
187    }
188 
189    // Ssss, sss.
190    template Snake(string str)
191    {
192       static if (str.length < 2)
193          enum Snake = str;
194       else static if (IsLower!(str[0]) && IsUpper!(str[1]))
195          enum Snake = str[0] ~ "_" ~ str[1] ~ SnakeCase!(str[2 .. $]);
196       else
197          enum Snake = str[0] ~ SnakeCase!(str[1 .. $]);
198    }
199 
200    enum SnakeCase = Snake!str.toLower;
201 }
202 
203 unittest
204 {
205    writeln(" * Attributes");
206    writeln("\t * SnakeCase");
207 
208    static assert(SnakeCase!"something" == "something");
209    static assert(SnakeCase!"UPPERCASE" == "uppercase");
210    static assert(SnakeCase!"camelCase" == "camel_case");
211    static assert(SnakeCase!"UpperCamelCase" == "upper_camel_case");
212    static assert(SnakeCase!"someTHING" == "some_thing");
213    static assert(SnakeCase!"some_thing" == "some_thing");
214 }
215 
216 /**
217    Relation/type name for the given type, can be set with @relation attribute.
218    If @relation is not set, type's name will be lower_snake_cased.
219  */
220 template relationName(alias R)
221 {
222    static if (hasUDA!(R, RelationAttribute))
223    {
224       enum rName = getUDAs!(R, RelationAttribute)[0].name;
225       static if (rName.length == 0)
226          enum relationName = SnakeCase!(R.stringof);
227       else
228          enum relationName = rName;
229    }
230    else
231       enum relationName = SnakeCase!(R.stringof);
232 }
233 
234 unittest
235 {
236    writeln("\t * relationName");
237 
238    struct Test {}
239    struct someThing {}
240    @relation("some_random_name") struct Test2 {}
241 
242    static assert(relationName!Test == "test");
243    static assert(relationName!someThing == "some_thing");
244    static assert(relationName!Test2 == "some_random_name");
245 }
246 
247 /**
248    Attribute name for the given type, can be specified with @attribute.
249    If @attribute is not specified, the member's name will just be
250    lower_snake_cased and returned.
251  */
252 template attributeName(alias R)
253 {
254    static if (hasUDA!(R, AttributeAttribute))
255       enum attributeName = getUDAs!(R, AttributeAttribute)[0].name;
256    else
257       enum attributeName = SnakeCase!(__traits(identifier, R));
258 }
259 
260 unittest
261 {
262    writeln("\t * attributeName");
263    struct Test
264    {
265       string id;
266       string someName;
267       @attr("someRandomName") string a;
268    }
269 
270    static assert(attributeName!(Test.id) == "id");
271    static assert(attributeName!(Test.someName) == "some_name");
272    static assert(attributeName!(Test.a) == "someRandomName");
273 }
274 
275 
276 /**
277  Workaround for getSymbolsByUDA not working on structs/classes with private members.
278  Returns all the structure's members that have the given UDA.
279  */
280 template getMembersByUDA(T, alias attribute)
281 {
282    import std.meta : Filter;
283 
284    enum hasSpecificUDA(string name) = mixin("hasUDA!(T." ~ name ~ ", attribute)");
285    alias getMembersByUDA = Filter!(hasSpecificUDA, __traits(allMembers, T));
286 }
287 
288 unittest
289 {
290    writeln("\t * getMembersByUDA");
291 
292    struct Test
293    {
294       @PK int id;
295       @FK!Test int id2;
296       @FK!Test int id3;
297    }
298 
299    alias FKMembers = getMembersByUDA!(Test, ForeignKeyAttribute);
300    alias PKMembers = getMembersByUDA!(Test, PrimaryKeyAttribute);
301 
302    static assert(PKMembers.length == 1);
303    static assert(PKMembers[0] == "id");
304 
305    static assert(FKMembers.length == 2);
306    static assert(FKMembers[0] == "id2");
307    static assert(FKMembers[1] == "id3");
308 
309    static assert(getMembersByUDA!(Test, IgnoreAttribute).length == 0);
310 }
311 
312 /**
313    Returns a string containing the name of the type member that is marked with @PK
314  */
315 template primaryKeyName(T)
316 {
317    alias fields = getMembersByUDA!(T, PrimaryKeyAttribute);
318    static assert(fields.length < 2,
319          "Multiple or composite primary key found for " ~ T.stringof ~ ", this is not currently supported");
320    static assert(fields.length == 1, "No primary key found for " ~ T.stringof);
321 
322    enum primaryKeyName = mixin(fields[0].stringof);
323 }
324 
325 /**
326    Returns the name of the PK attribute (SQL name)
327  */
328 template primaryKeyAttributeName(T)
329 {
330    enum primaryKeyAttributeName = attributeName!(mixin("T." ~ primaryKeyName!T));
331 }
332 
333 unittest
334 {
335    writeln("\t * primaryKeyName");
336    struct Test
337    {
338       @PK int myPK;
339       int id;
340    }
341 
342    static assert(primaryKeyName!Test == "myPK");
343 
344    writeln("\t * primaryKeyAttributeName");
345 
346    static assert(primaryKeyAttributeName!Test == "my_pk");
347 }
348 
349 /**
350    Returns true if the member m is a PK on T.
351  */
352 template isPK(alias T, string m)
353 {
354    enum isPK = hasUDA!(mixin("T." ~ m), PrimaryKeyAttribute);
355 }
356 
357 unittest
358 {
359    writeln("\t * isPK");
360    struct Test
361    {
362       @PK int id;
363       string a;
364    }
365 
366    static assert(isPK!(Test, "id"));
367    static assert(!isPK!(Test, "a"));
368 }
369 
370 deprecated template embeddedPrefix(T, string name)
371 {
372    import std.string : format;
373    enum embeddedPrefix ="_%s_%s_".format(
374          relationName!T,
375          SnakeCase!name);
376 }
377 
378 /**
379    Returns a list of Columns for all the given type's serialisable members,
380    with their actual names as they're used in SQL.
381 
382    Params:
383       prefix   = prefix to use, if any
384       asPrefix = asPrefix, prefix to use for column's AS names
385       ignorePK = whether to ignore the PK, useful for inserts and similar
386  */
387 template AttributeList2(
388       T,
389       string prefix = "",
390       string asPrefix = "",
391       bool ignorePK = false,
392       fields...)
393 {
394    static if (fields.length == 0)
395       enum AttributeList2 = [];
396    else
397    {
398       alias mt = typeof(mixin("T." ~ fields[0]));
399 
400       // Ignore the PK
401       static if (ignorePK && isPK!(T, fields[0]) || hasUDA!(IgnoreAttribute, mixin("T." ~ fields[0])))
402          enum AttributeList2 = AttributeList2!(T, prefix, asPrefix, ignorePK, fields[1 .. $]);
403       else
404       {
405          enum attrName = attributeName!(mixin("T." ~ fields[0]));
406          enum AttributeList2 =
407                [Column(prefix ~ attrName, asPrefix ~ attrName)] ~
408                AttributeList2!(
409                      T,
410                      prefix,
411                      asPrefix,
412                      ignorePK,
413                      fields[1 .. $]);
414       }
415    }
416 }
417 
418 template AttributeList(T, bool ignorePK = false, bool insert = false)
419 {
420    alias AttributeList = AttributeList2!(T, "", "", ignorePK, serialisableMembers!(T));
421    static assert(AttributeList.length > 0, "AttributeList found no fields, for " ~ T.stringof ~ " cannot continue");
422 }
423 
424 unittest
425 {
426    import std.typecons : Nullable;
427    writeln("\t * AttributeList");
428    struct Test2
429    {
430       string bar;
431       string baz;
432    }
433 
434    struct Test
435    {
436       @PK int id;
437       Test2 inner;
438    }
439 
440    alias attrs = AttributeList!Test;
441    static assert(attrs[0] == Column("id", "id"));
442    static assert(attrs[1] == Column("inner", "inner"));
443 
444    // ignorePK
445    static assert(AttributeList!(Test, true)[0] == Column("inner", "inner"));
446 
447    // INSERT syntax, with ignorePK
448    static assert(AttributeList!(Test, true)[0] == Column("inner", "inner"));
449 }
450 
451 /**
452    Gives a list of all the structure's members that will be used in the DB.
453    Ignores @ignore members, non-RW and non-public members.
454  */
455 template serialisableMembers(T)
456 {
457    alias RT = RealType!T;
458    alias serialisableMembers = filterSerialisableMembers!(RT, __traits(allMembers, RT));
459 }
460 
461 unittest
462 {
463    writeln("\t * serialisableMembers");
464    struct Test
465    {
466       int a;
467       private int _b;
468       @property int b() {return _b;};
469       @property void b(int x) {_b = x;};
470       @property int c() {return _b;};
471    }
472 
473    static assert(serialisableMembers!Test.length == 2);
474    static assert(serialisableMembers!Test[0] == "a");
475    static assert(serialisableMembers!Test[1] == "b");
476 }
477 
478 /// A filter implementation for serialisableMembers
479 template filterSerialisableMembers(T, fields...)
480 {
481    static if (fields.length == 0)
482       alias filterSerialisableMembers = TypeTuple!();
483    else
484    {
485       enum m = fields[0];
486       static if (isRWPlainField!(T, m) || isRWField!(T, m))
487       {
488          static if (!hasUDA!(mixin("T." ~ m), IgnoreAttribute)) {
489             alias filterSerialisableMembers = TypeTuple!(
490                   TypeTuple!(m),
491                   filterSerialisableMembers!(T, fields[1 .. $]));
492          } else {
493             alias filterSerialisableMembers = filterSerialisableMembers!(T, fields[1 .. $]);
494          }
495       }
496       else
497          alias filterSerialisableMembers = filterSerialisableMembers!(T, fields[1 .. $]);
498    }
499 }
500 
501 
502 /*
503    Functions/templates isRWPlainField, isRWField, isPublicMember and isNonStaticMember.
504 
505    Extensions to `std.traits` module of Phobos. Some may eventually make it into Phobos,
506    some are dirty hacks that work only for vibe.d
507    Copyright: © 2012 RejectedSoftware e.K.
508    License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
509    Authors: Sönke Ludwig, Михаил Страшун
510 */
511 
512 
513 /**
514    Determins if a member is a public, non-static data field.
515 */
516 
517 template isRWPlainField(T, string M)
518 {
519    static if (!isRWField!(T, M))
520       enum isRWPlainField = false;
521    else
522       enum isRWPlainField = __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M)));
523 }
524 
525 /**
526    Determines if a member is a public, non-static, de-facto data field.
527    In addition to plain data fields, R/W properties are also accepted.
528 */
529 template isRWField(T, string M)
530 {
531    import std.traits;
532    import std.typetuple;
533 
534    static void testAssign()()
535    {
536       T t = void;
537       __traits(getMember, t, M) = __traits(getMember, t, M);
538    }
539 
540    // reject type aliases
541    static if (is(TypeTuple!(__traits(getMember, T, M))))
542       enum isRWField = false;
543    // reject non-public members
544    else static if (!isPublicMember!(T, M))
545       enum isRWField = false;
546    // reject static members
547    else static if (!isNonStaticMember!(T, M))
548       enum isRWField = false;
549    // reject non-typed members
550    else static if (!is(typeof(__traits(getMember, T, M))))
551       enum isRWField = false;
552    // reject void typed members (includes templates)
553    else static if (is(typeof(__traits(getMember, T, M)) == void))
554       enum isRWField = false;
555    // reject non-assignable members
556    else static if (!__traits(compiles, testAssign!()()))
557       enum isRWField = false;
558    else static if (anySatisfy!(isSomeFunction, __traits(getMember, T, M)))
559    {
560       // If M is a function, reject if not @property or returns by ref
561       private enum FA = functionAttributes!(__traits(getMember, T, M));
562       enum isRWField = (FA & FunctionAttribute.property) != 0;
563    }
564    else
565    {
566       enum isRWField = true;
567    }
568 }
569 
570 template isPublicMember(T, string M)
571 {
572    import std.algorithm, std.typetuple : TypeTuple;
573 
574    static if (!__traits(compiles, TypeTuple!(__traits(getMember, T, M))))
575       enum isPublicMember = false;
576    else
577    {
578       alias MEM = TypeTuple!(__traits(getMember, T, M));
579       enum isPublicMember = __traits(getProtection, MEM).among("public", "export");
580    }
581 }
582 
583 template isNonStaticMember(T, string M)
584 {
585    import std.typetuple;
586    import std.traits;
587 
588    alias MF = TypeTuple!(__traits(getMember, T, M));
589 
590    static if (M.length == 0)
591    {
592        enum isNonStaticMember = false;
593    }
594    else static if (anySatisfy!(isSomeFunction, MF))
595    {
596        enum isNonStaticMember = !__traits(isStaticFunction, MF);
597    }
598    else
599    {
600       enum isNonStaticMember = !__traits(compiles, (){ auto x = __traits(getMember, T, M); }());
601    }
602 }