1 module cucumber.reflection;
2 
3 import cucumber.ctutils;
4 import cucumber.keywords;
5 import std.traits;
6 import std.typetuple;
7 import std.regex;
8 import std.conv;
9 
10 
11 private template isMatchStruct(alias T) {
12     static if(__traits(compiles, typeof(T))) {
13         enum isMatchStruct = is(typeof(T): Match);
14     } else {
15         enum isMatchStruct = false;
16     }
17 }
18 
19 unittest {
20     Match match;
21     static assert(isMatchStruct!match);
22 }
23 
24 
25 private template hasMatchUDA(alias T) {
26     alias attrs = Filter!(isMatchStruct, __traits(getAttributes, T));
27     static assert(attrs.length < 2, "Only one Match UDA per function");
28     enum hasMatchUDA = attrs.length == 1;
29 }
30 
31 private auto getRegex(alias T)() {
32     static assert(hasMatchUDA!T, "Can only get regexp from Match structure");
33     return Filter!(isMatchStruct, __traits(getAttributes, T))[0].reg;
34 }
35 
36 unittest {
37     @Match(`^foo reg`)
38     void foo() {
39         static assert(hasMatchUDA!foo);
40         static assert(getRegex!foo == `^foo reg`);
41     }
42 }
43 
44 auto getLineNumber(alias T)() {
45     static assert(hasMatchUDA!T, "Can only get line number from Match structure");
46     return Filter!(isMatchStruct, __traits(getAttributes, T))[0].line;
47 }
48 
49 unittest {
50     @Match("foo", 4) void foo() {}
51     static assert(getLineNumber!foo == 4);
52 
53     @Match("bar", 3) void bar() {}
54     static assert(getLineNumber!bar == 3);
55 }
56 
57 alias CucumberStepFunction = void function(in string[] = []);
58 
59 struct CucumberStep {
60     this(in CucumberStepFunction func, in string reg, int id, in string source) {
61         this(func, std.regex.regex(reg), id, source);
62         this.regexString = reg;
63     }
64 
65     this(in CucumberStepFunction func, Regex!char reg, int id, in string source) {
66         this.func = func;
67         this.regex = reg;
68         this.id = id;
69         this.source = source;
70     }
71 
72     CucumberStepFunction func;
73     Regex!char regex;
74     string regexString;
75     int id; //automatically generated id
76     string source;
77 }
78 
79 
80 /**
81  * Finds all steps in all modules. Modules are passed in as strings.
82  * Steps are found by using compile-time reflection to register
83  * all functions with the Match UDA attached to it and extracting
84  * the relevant regex from the Match UDA itself.
85  */
86 auto findSteps(ModuleNames...)() if(allSatisfy!(isSomeString, (typeof(ModuleNames)))) {
87     mixin(importModulesString!ModuleNames);
88     CucumberStep[] cucumberSteps;
89     int id;
90     foreach(mod; ModuleNames) {
91         foreach(member; __traits(allMembers, mixin(mod))) {
92 
93             enum compiles = __traits(compiles, mixin(member));
94 
95             static if(compiles) {
96 
97                 enum isFunction = isSomeFunction!(mixin(member));
98                 enum hasMatch = hasMatchUDA!(mixin(member));
99 
100                 static if(isFunction && hasMatch) {
101                     enum reg = rawStringMixin(getRegex!(mixin(member)));
102 
103                     enum funcArity = arity!(mixin(member));
104                     enum numCaptures = countParenPairs!reg;
105                     static assert(funcArity == numCaptures,
106                                   text("Arity of ", member, " (", funcArity, ")",
107                                        " does not match the number of capturing parens (",
108                                        numCaptures, ") in ", getRegex!(mixin(member))));
109 
110                     //e.g. funcCall would be "myfunc(captures[0], captures[1]);"
111                     enum funcCall = member ~ argsStringWithParens!(reg, mixin(member)) ~ ";";
112 
113                     //e.g. lambda would be "(captures) { myfunc(captures[0]); }"
114                     enum lambda = "(captures) { " ~ funcCall ~ " }";
115 
116                     //e.g. mymod.myfunc:13
117                     enum source = `"` ~ mod ~ "." ~ member ~ `:` ~ getLineNumber!(mixin(member)).to!string ~ `"`;
118 
119                     //e.g. cucumberSteps ~= CucumberStep((in string[] cs) { myfunc(); }, r"foobar",
120                     //                                   ++id, "foo.bar:3");
121                     enum mixinStr = `cucumberSteps ~= CucumberStep(` ~ lambda ~ `, ` ~ reg ~
122                                                                    `, ++id, ` ~ source ~ `);`;
123 
124                     mixin(mixinStr);
125                 }
126             }
127         }
128     }
129 
130     return cucumberSteps;
131 }
132 
133 /**
134  * Normally this struct wouldn't exist and I'd use a delegate.
135  * But for the wire protocol I need access to the captures
136  * array so it's a struct.
137  */
138 struct MatchResult {
139     CucumberStepFunction func;
140     const (string)[] captures;
141     int id;
142     string regex;
143     string source;
144     this(in CucumberStepFunction func, in string[] captures, in int id, in string regex, in string source) {
145         this.func = func;
146         this.captures = captures;
147         this.id = id;
148         this.regex = regex;
149         this.source = source;
150     }
151 
152     void opCall() const {
153         if(func is null) throw new Exception("MatchResult with null function");
154         func(captures);
155     }
156 
157     bool opCast(T: bool)() {
158         return func !is null;
159     }
160 }
161 
162 
163 /**
164  * Finds the match to a step string. Checks all steps and loops
165  * over to see which one has a matching regex. Steps are found
166  * at compile-time.
167  */
168 MatchResult findMatch(ModuleNames...)(string step_str) {
169     step_str = stripCucumberKeywords(step_str);
170     enum steps = findSteps!ModuleNames;
171     foreach(step; steps) {
172         auto m = step_str.match(step.regex);
173         if(m) {
174             import std.array;
175             return MatchResult(step.func, m.captures.array, step.id, step.regexString, step.source);
176         }
177     }
178 
179     return MatchResult();
180 }
181 
182 
183 /**
184  * Counts the number of parentheses pairs in a string known
185  * at compile-time
186  */
187 int countParenPairs(string reg)() {
188     int intCount(in string haystack, in string needle) {
189         import std.algorithm: count;
190         return cast(int)haystack.count(needle);
191     }
192 
193     //no need to count matching closing parens since it won't be a valid
194     //regexp and will throw an exception at runtime anyway
195     return intCount(reg, "(") - intCount(reg, r"\(") - intCount(reg, r"(?:");
196 }
197 
198 unittest {
199     static assert(countParenPairs!r"" == 0);
200     static assert(countParenPairs!r"foo" == 0);
201     static assert(countParenPairs!r"\(\)" == 0);
202     static assert(countParenPairs!r"()" == 1);
203     static assert(countParenPairs!r"()\(\)" == 1);
204     static assert(countParenPairs!r"\(\)()" == 1);
205     static assert(countParenPairs!r"()\(\)()" == 2);
206     static assert(countParenPairs!r"(foo).+\(oh noes\).+(bar)" == 2);
207     static assert(countParenPairs!r"(.+).+(?:oh noes).+" == 1);
208 }
209 
210 /**
211  * Returns an array of string mixins to convert each type
212  * from a string
213  */
214 auto conversionsFromString(Types...)() {
215     string[] convs;
216     foreach(T; Types) {
217         static if(isSomeString!T) {
218             convs ~= "";
219         } else {
220             convs ~= ".to!" ~ Unqual!T.stringof;
221         }
222     }
223     return convs;
224 }
225 
226 unittest {
227     static assert(conversionsFromString!(int, string) == [".to!int", ""]);
228     static assert(conversionsFromString!(string, double) == ["", ".to!double"]);
229 }
230 
231 /**
232  * Comma separated argument list for calls to variadic functions
233  * associated with regexen. Meant to be used with mixin to
234  * generate code.
235  */
236 string argsString(string reg, alias func)() {
237     enum convs = conversionsFromString!(ParameterTypeTuple!func);
238     enum numCaptures = countParenPairs!reg;
239     static assert(convs.length == numCaptures,
240                   text("Wrong length for ", convs, ", should be ", numCaptures));
241 
242     string[] args;
243     foreach(i; 0 .. numCaptures) {
244         args ~= "captures[" ~ (i + 1).to!string ~ "]" ~ convs[i];
245     }
246 
247     import std.array;
248     return args.join(", ");
249 }
250 
251 string argsStringWithParens(string reg, alias func)() {
252     return "(" ~ argsString!(reg, func) ~ ")";
253 }
254 
255 unittest {
256     void func() {}
257     static assert(argsString!(r"", func) == "");
258 
259     void func_is(int, string) {}
260     static assert(argsString!(r"(foo)...(bar)", func_is) == "captures[1].to!int, captures[2]");
261 
262     void func_cis(in int, in string) {}
263     static assert(argsString!(r"(foo)...(bar)", func_cis) == "captures[1].to!int, captures[2]");
264 
265     void func_si(string, int) {}
266     static assert(argsString!(r"(foo)...(bar)", func_si) == "captures[1], captures[2].to!int");
267 
268     void func_dd(double, double) {}
269     static assert(argsString!(r"(foo)...(bar)", func_dd) == "captures[1].to!double, captures[2].to!double");
270 }
271 
272 /**
273  * This function is necessary due to the extreme "metaness" of
274  * this project. It returns a string that is meant to be consumed
275  * by mixin to generate code. Since the strings being fed to
276  * the system at compile-time are generally going to be
277  * "raw" strings, they'll either be r"" or `` style D strings.
278  * If the regex requires the '"' character they'll use ``, and
279  * vice-versa. When generating code, I opted for r"" strings.
280  * This would cause a compilation error if the regex has the
281  * quote character in it. So this function returns an escaped
282  * version that can be safely used.
283  */
284 string rawStringMixin(in string str) {
285     import std.array;
286     return "r\"" ~ str.replace(`"`, "\" ~ `\"` ~ r\"") ~ "\"";
287 }
288 
289 unittest {
290     static assert(rawStringMixin(`Sharks with "lasers" yo.`) ==
291                   "r\"Sharks with \" ~ `\"` ~ r\"lasers\" ~ `\"` ~ r\" yo.\"");
292     static assert(rawStringMixin(`foo bar baz`) == "r\"foo bar baz\"");
293 }