1 /**
2     Provides utilites that allow you to enforce signatures - a specification for a structure
3 */
4 module bolts.experimental.signatures;
5 
6 ///
7 unittest {
8     interface InputRange(T) {
9         @property bool empty();
10         @property T front();
11         @ignoreAttributes void popFront();
12     }
13 
14     struct R(T) {
15         mixin Models!(InputRange!T);
16 
17         T[] values;
18         int index;
19         this(T[] arr) {
20             values = arr;
21         }
22         @property bool empty() {
23             return values.length == index;
24         }
25         @property T front() {
26             return values[index];
27         }
28         void popFront() {
29             index++;
30         }
31     }
32 
33     import std.range: array;
34     auto r = R!int([1, 4, 2, 3]);
35     assert(r.array == [1, 4, 2, 3]);
36 }
37 
38 private enum Report {
39     all,
40     one,
41 }
42 
43 private auto checkSignatureOf(alias Model, alias Sig, Report report = Report.one, string path = "")() {
44     import bolts.traits: StringOf;
45     import bolts.meta: RemoveAttributes;
46     import std.traits: hasMember, isAggregateType, isNested, OriginalType;
47     import std.conv: to;
48 
49     alias sigMember(string member) = __traits(getMember, Sig, member);
50     alias modelMember(string member) = __traits(getMember, Model, member);
51 
52     string typeToString(T)() {
53         import std.traits: isFunction;
54         static if (is(T == struct)) {
55             return "struct";
56         } else static if (is(T == class)) {
57             return "class";
58         } else static if (is(T == union)) {
59             return "union";
60         } else static if (is(T == interface)) {
61             return "interface";
62         } else static if (is(T == enum)) {
63             return "enum";
64         } else static if (isFunction!T) {
65             return "function type";
66         } else {
67             return "type";
68         }
69     }
70 
71     string errorPrefix(string prefix)() {
72         string lower() {
73             if (prefix[0] >= 'A' && prefix[0] <= 'Z') {
74                 return prefix[0] + 32 ~ prefix[1 .. $];
75             } else {
76                 return prefix;
77             }
78         }
79 
80         string upper() {
81             if (prefix[0] >= 'a' && prefix[0] <= 'z') {
82                 return prefix[0] - 32 ~ prefix[1 .. $];
83             } else {
84                 return prefix;
85             }
86         }
87 
88         if (path.length) {
89             return "Type `" ~ path ~ "` " ~ lower();
90         } else {
91             return upper();
92         }
93     }
94 
95     string checkTypedIdentifier(string member, SigMember)() {
96 
97         import std.traits: isFunction, hasUDA, hasStaticMember;
98 
99         enum error = errorPrefix!"Missing identifier `"
100             ~ member
101             ~ "` of "
102             ~ typeToString!SigMember
103             ~ " `"
104             ~ (hasStaticMember!(Sig, member) ? "static " : "")
105             ~ StringOf!SigMember
106             ~ "`.";
107 
108         bool staticCheckPass() {
109             return !hasStaticMember!(Sig, member)
110                 || hasStaticMember!(Model, member);
111         }
112 
113         static if (is(typeof(modelMember!member) ModelMember)) {
114 
115             if (!staticCheckPass()) {
116                 return error;
117             }
118 
119             static if (hasUDA!(sigMember!member, ignoreAttributes)) {
120                 alias T = RemoveAttributes!SigMember;
121                 alias U = RemoveAttributes!ModelMember;
122             } else {
123                 static if (hasUDA!(sigMember!member, ignoreQualifiers)) {
124                     import std.traits: Unqual;
125                     alias T = Unqual!SigMember;
126                     alias U = Unqual!ModelMember;
127                 } else {
128                     alias T = SigMember;
129                     alias U = ModelMember;
130                 }
131             }
132 
133             static if (is(T == U)) {
134                 return null;
135             } else {
136                 return error;
137             }
138 
139         } else {
140             return error;
141         }
142     }
143 
144     string checkEnum(string member, ModelMember, SigMember)() {
145         static if (is(ModelMember == enum) && is(OriginalType!SigMember == OriginalType!ModelMember)) {
146             import std.algorithm: sort, setDifference;
147             auto sigMembers = [__traits(allMembers, SigMember)].sort;
148             auto modelMembers = [__traits(allMembers, ModelMember)].sort;
149             if (sigMembers != modelMembers) {
150                 return errorPrefix!"Enum `"
151                     ~ member
152                     ~ "` is missing members: "
153                     ~ sigMembers.setDifference(modelMembers).to!string;
154             }
155             return null;
156         } else {
157             return errorPrefix!"Missing enum named `"
158                 ~ member
159                 ~ "` of type `"
160                 ~ StringOf!SigMember
161                 ~ " with original type `"
162                 ~ StringOf!(OriginalType!SigMember)
163                 ~ "`.";
164         }
165     }
166 
167     string checkAlias(string member, ModelMember, SigMember)() {
168         static if (!is(SigMember == ModelMember)) {
169             return errorPrefix!"Found alias `"
170                 ~ member
171                 ~ "` of wrong type. Expected alias to "
172                 ~ typeToString!SigMember
173                 ~ " `"
174                 ~ StringOf!SigMember
175                 ~ "`.";
176         } else {
177             return null;
178         }
179     }
180 
181     auto checkType(string member, SigMember)() {
182         static if (is(modelMember!member ModelMember)) {
183             static if (member != StringOf!SigMember) {
184                 if (auto error = checkAlias!(member, ModelMember, SigMember)) {
185                     return error;
186                 }
187             } else static if (is(SigMember == enum)) {
188                 if (auto error = checkEnum!(member, ModelMember, SigMember)) {
189                     return error;
190                 }
191             } else static if (isAggregateType!SigMember) {
192                 if (auto error = checkSignatureOf!(ModelMember, SigMember, report, path ~ "." ~ StringOf!ModelMember)) {
193                     return error;
194                 }
195             }
196             return null;
197         } else {
198             static if (StringOf!SigMember != member) {
199                 return errorPrefix!"Missing alias named `"
200                     ~ member
201                     ~ "` to "
202                     ~ typeToString!SigMember
203                     ~ " `"
204                     ~ StringOf!SigMember
205                     ~ "`.";
206             } else {
207                 return errorPrefix!"Missing "
208                     ~ typeToString!SigMember
209                     ~ " named `"
210                     ~ member
211                     ~ "`";
212             }
213         }
214     }
215 
216     string checkUnknown(string member)() {
217         static if (isNested!Sig && member == "this") {
218             return null;
219         } else {
220             return "Don`t know member `" ~ member ~ "` of type `" ~ StringOf!Model ~ "`";
221         }
222     }
223 
224     static if (report == Report.all) {
225         string[] result;
226     } else {
227         string result;
228     }
229 
230     immutable storeResult = q{
231         static if (report == Report.one) {
232             result = error;
233             break;
234         } else {
235             result ~= error;
236         }
237     };
238 
239     foreach (member; __traits(allMembers, Sig)) {
240         static if (is(typeof(sigMember!member) T)) {
241             if (auto error = checkTypedIdentifier!(member, T)) {
242                 mixin(storeResult);
243             }
244         } else static if (is(sigMember!member T)) {
245             if (auto error = checkType!(member, T)) {
246                 mixin(storeResult);
247             }
248         } else {
249             if (auto error = checkUnknown!member) {
250                 mixin(storeResult);
251             }
252         }
253     }
254     return result;
255 }
256 
257 unittest {
258     struct X {
259         alias b = int; alias c = float; enum E1 { one } void f(int) {}
260         enum E2 { a, b } int x; float y; short z; enum E3 { a, b }
261         struct Inner {
262             struct AnotherInner {
263                 int a;
264                 int b;
265             }
266         }
267         struct MissingInner {
268             struct A {}
269         }
270         static int s;
271     }
272     struct Y {
273         alias a = int; alias c = int;
274         enum E2 { a }    int x;          int z;   enum E3 { a, b }
275         struct Inner {
276             struct AnotherInner {
277                 int a;
278                 int b;
279             }
280         }
281         struct MissingInner {}
282     }
283 
284     const expectedErrors = [
285         "Missing alias named `b` to type `int`.",
286         "Found alias `c` of wrong type. Expected alias to type `float`.",
287         "Missing enum named `E1`",
288         "Missing identifier `f` of function type `void(int)`.",
289         "Enum `E2` is missing members: [\"b\"]",
290         "Missing identifier `y` of type `float`.",
291         "Missing identifier `z` of type `short`.",
292         "Type `.MissingInner` missing struct named `A`",
293         "Missing identifier `s` of type `static int`.",
294     ];
295 
296     assert(checkSignatureOf!(Y, X, Report.all) == expectedErrors);
297 }
298 
299 /**
300     Checks if type `Model` is a model of type `Sig`
301 */
302 template isModelOf(alias _Model, alias _Sig) {
303     import bolts.traits: TypesOf;
304     alias Model = TypesOf!_Model[0];
305     alias Sig = TypesOf!_Sig[0];
306     enum isModelOf = checkSignatureOf!(Model, Sig, Report.one) == null;
307 }
308 
309 /**
310     Asserts that the given model follows the specification of the given signature
311 */
312 template AssertModelOf(alias _Model, alias _Sig, string file = __FILE__, int line = __LINE__) {
313     import std.algorithm: map, joiner;
314     import std.range: array;
315     import std.conv: to;
316     import bolts.traits: StringOf, TypesOf;
317 
318     alias Model = TypesOf!_Model[0];
319     alias Sig = TypesOf!_Sig[0];
320 
321     string addLocation(string str) {
322         template symLoc(alias sym) {
323             template format(string file, int line, int _) {
324                 enum format = file ~ "(" ~ to!string(line) ~ ")";
325             }
326             enum symLoc = format!(__traits(getLocation, sym));
327         }
328         enum assertLoc = file ~ "(" ~ to!string(line) ~ ")";
329         return str
330             ~ "\n  "
331             ~ symLoc!Sig
332             ~ ": <-- Signature `"
333             ~ StringOf!Sig
334             ~ "` defined here.\n  "
335             ~ assertLoc
336             ~ ": <-- Checked here.";
337     }
338 
339     immutable errors = checkSignatureOf!(Model, Sig, Report.all);
340 
341     static assert(
342         errors.length == 0,
343         "Type `" ~ StringOf!Model ~ "` does not comply to signature `" ~ StringOf!Sig ~ "`"
344             ~ errors
345                 .map!(s => "\n  " ~ s)
346                 .joiner
347                 .to!string
348                 .addLocation
349     );
350 
351     enum AssertModelOf = true;
352 }
353 
354 ///
355 unittest {
356     struct X { int a; float z; }
357     struct Y { int a; float z; }
358     struct Z { int b; float z; }
359 
360     static assert(isModelOf!(Y, X));
361 }
362 
363 /**
364     Mixin that ensures a type models the desired signature of a structure
365 */
366 mixin template Models(alias Sig, string file = __FILE__, int line = __LINE__) {
367     static import bolts.experimental;
368     static assert(bolts.experimental.signatures.AssertModelOf!(typeof(this), Sig, file, line));
369 }
370 
371 ///
372 unittest {
373     struct Sig {
374         alias I = int;
375         int x;
376         float y;
377         struct Inner { int a; struct X { int b; } }
378         int f(int) { return 0; }
379         enum X { one, two }
380         union L { int a; }
381     }
382 
383     struct Y {
384         mixin Models!Sig;
385         alias I = int;
386         int x;
387         float y;
388         struct Inner { int a; struct X { int b; } }
389         int f(int) { return 0; }
390         enum X { one, two }
391         union L { int a; }
392     }
393 
394     static assert(isModelOf!(Y, Sig));
395 }
396 
397 unittest {
398     struct TemplatedSig(T) {
399         T value;
400     }
401 
402     struct Y(T) {
403         mixin Models!(TemplatedSig!T);
404         T value;
405     }
406 
407     static assert(__traits(compiles, {
408         Y!int x;
409     }));
410 }
411 
412 unittest {
413     struct Sig {
414         alias I = int;
415         int x;
416         float y;
417     }
418 
419     static assert(!__traits(compiles, {
420         struct X {
421             mixin Models!Sig;
422             alias I = float;
423             int x;
424             float y;
425         }
426     }));
427 
428     static assert(!__traits(compiles, {
429         struct X {
430             mixin Models!Sig;
431             int I;
432             int x;
433             float y;
434         }
435     }));
436 
437     static assert(!__traits(compiles, {
438         struct X {
439             mixin Models!Sig;
440             alias M = int;
441             int x;
442             float y;
443         }
444     }));
445 }
446 
447 unittest {
448     struct Sig {
449         alias I = int;
450         int x;
451         float y;
452     }
453 
454     static assert(!__traits(compiles, {
455         struct X {
456             mixin Models!Sig;
457             alias I = float;
458             float x;
459             float y;
460         }
461     }));
462 
463     static assert(!__traits(compiles, {
464         struct X {
465             mixin Models!Sig;
466             int I;
467             alias x = int;
468             float y;
469         }
470     }));
471 
472     static assert(!__traits(compiles, {
473         struct X {
474             mixin Models!Sig;
475             alias M = int;
476             int x;
477         }
478     }));
479 }
480 
481 /**
482     Attribute that can be applied on identifiers in a signature that will let the model checker know not to
483     take attributes in to account
484 */
485 struct ignoreAttributes {}
486 
487 /**
488     Attribute that can be applied on identifiers in a signature that will let the model checker know not to
489     take type qualifiers in to account
490 */
491 struct ignoreQualifiers {}
492 
493 unittest {
494     interface Sig {
495         static @property string name();
496         @ignoreQualifiers static @property string help();
497         int run(string[]);
498     }
499 
500     struct X {
501         mixin Models!Sig;
502 
503         static string name = "hello";
504         immutable static string help = "help";
505         int run(string[] args) {
506             return 0;
507         }
508     }
509 }