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 import gherkin.docstring : DocString;
11 import gherkin.datatable : DataTable;
12 
13 private template isMatchStruct(alias T)
14 {
15     static if (__traits(compiles, typeof(T)))
16     {
17         enum isMatchStruct = is(typeof(T) : Match);
18     }
19     else
20     {
21         enum isMatchStruct = false;
22     }
23 }
24 
25 unittest
26 {
27     const Match match;
28     static assert(isMatchStruct!match);
29 }
30 
31 private template hasMatchUDA(alias T)
32 {
33     alias attrs = Filter!(isMatchStruct, __traits(getAttributes, T));
34     static assert(attrs.length < 2, "Only one Match UDA per function");
35     enum hasMatchUDA = attrs.length == 1;
36 }
37 
38 private auto getRegex(alias T)()
39 {
40     static assert(hasMatchUDA!T, "Can only get regexp from Match structure");
41     return Filter!(isMatchStruct, __traits(getAttributes, T))[0].reg;
42 }
43 
44 unittest
45 {
46     @Match(`^foo reg`)
47     void foo()
48     {
49         static assert(hasMatchUDA!foo);
50         static assert(getRegex!foo == `^foo reg`);
51     }
52 }
53 
54 auto getLineNumber(alias T)()
55 {
56     static assert(hasMatchUDA!T, "Can only get line number from Match structure");
57     return Filter!(isMatchStruct, __traits(getAttributes, T))[0].line;
58 }
59 
60 unittest
61 {
62     @Match("foo", 4) void foo()
63     {
64     }
65 
66     static assert(getLineNumber!foo == 4);
67 
68     @Match("bar", 3) void bar()
69     {
70     }
71 
72     static assert(getLineNumber!bar == 3);
73 }
74 
75 alias CucumberStepFunction = void function(in string[] = []);
76 alias CucumberStepFunctionWithDocString = void function(in string[] = [], DocString = DocString());
77 alias CucumberStepFunctionWithDataTable = void function(in string[] = [], DataTable = DataTable());
78 
79 ///
80 struct CucumberStep
81 {
82     ///
83     this(in CucumberStepFunction func, in string reg, int id, in string source)
84     {
85         this(func, std.regex.regex(reg), id, source);
86         this.regexString = reg;
87     }
88 
89     ///
90     this(in CucumberStepFunctionWithDocString func, in string reg, int id, in string source)
91     {
92         this(func, std.regex.regex(reg), id, source);
93         this.regexString = reg;
94     }
95 
96     ///
97     this(in CucumberStepFunctionWithDataTable func, in string reg, int id, in string source)
98     {
99         this(func, std.regex.regex(reg), id, source);
100         this.regexString = reg;
101     }
102 
103     ///
104     this(T)(in T func, Regex!char reg, int id, in string source)
105     {
106         static if (is(T == CucumberStepFunction))
107         {
108             this.func = func;
109         }
110         else static if (is(T == CucumberStepFunctionWithDataTable))
111         {
112             this.funcWithDataTable = func;
113         }
114         else static if (is(T == CucumberStepFunctionWithDocString))
115         {
116             this.funcWithDocString = func;
117         }
118         this.regex = reg;
119         this.id = id;
120         this.source = source;
121     }
122 
123     ///
124     CucumberStepFunction func;
125     ///
126     CucumberStepFunctionWithDocString funcWithDocString;
127     ///
128     CucumberStepFunctionWithDataTable funcWithDataTable;
129     ///
130     Regex!char regex;
131     ///
132     string regexString;
133     /// automatically generated id
134     int id;
135     ///
136     string source;
137 }
138 
139 /**
140  * Finds all steps in all modules. Modules are passed in as strings.
141  * Steps are found by using compile-time reflection to register
142  * all functions with the Match UDA attached to it and extracting
143  * the relevant regex from the Match UDA itself.
144  */
145 auto findSteps(ModuleNames...)()
146         if (allSatisfy!(isSomeString, (typeof(ModuleNames))))
147 {
148     mixin(importModulesString!ModuleNames);
149     CucumberStep[] cucumberSteps;
150     int id;
151     foreach (mod; ModuleNames)
152     {
153         foreach (member; __traits(allMembers, mixin(mod)))
154         {
155 
156             enum compiles = __traits(compiles, mixin(member));
157 
158             static if (compiles)
159             {
160 
161                 enum isFunction = isSomeFunction!(mixin(member));
162                 enum hasMatch = hasMatchUDA!(mixin(member));
163 
164                 static if (isFunction && hasMatch)
165                 {
166                     enum reg = rawStringMixin(getRegex!(mixin(member)));
167 
168                     enum funcArity = arity!(mixin(member));
169                     enum numCaptures = countParenPairs!reg;
170 
171                     static if (is(Unconst!(Parameters!(mixin(member))[$ - 1]) == DocString)
172                             || is(Unconst!(Parameters!(mixin(member))[$ - 1]) == DataTable))
173                     {
174                         static assert(funcArity == numCaptures + 1, text("Arity of ", member, " (", funcArity, ")",
175                                 " does not match the number of capturing parens (",
176                                 numCaptures, " + 1) in ", getRegex!(mixin(member))));
177 
178                         static if (is(Unconst!(Parameters!(mixin(member))[$ - 1]) == DocString))
179                         {
180                             enum arg = "docString";
181                             enum type = "DocString";
182                         }
183                         else static if (
184                             is(Unconst!(Parameters!(mixin(member))[$ - 1]) == DataTable))
185                         {
186                             enum arg = "dataTable";
187                             enum type = "DataTable";
188                         }
189 
190                         //e.g. funcCall would be "myfunc(captures[0], captures[1], arg);"
191                         enum funcCall = member ~ "(" ~ argsString!(reg, mixin(member), arg) ~ ");";
192 
193                         //e.g. lambda would be "(in string[] captures, Arg arg) { myfunc(captures[0], captures[1], docString); }"
194                         enum lambda = "(in string[] captures, " ~ type ~ " "
195                             ~ arg ~ ") { " ~ funcCall ~ " }";
196                     }
197                     else
198                     {
199                         static assert(funcArity == numCaptures, text("Arity of ", member, " (", funcArity, ")",
200                                 " does not match the number of capturing parens (",
201                                 numCaptures, ") in ", getRegex!(mixin(member))));
202 
203                         //e.g. funcCall would be "myfunc(captures[0], captures[1]);"
204                         enum funcCall = member ~ argsStringWithParens!(reg, mixin(member)) ~ ";";
205 
206                         //e.g. lambda would be "(captures) { myfunc(captures[0]); }"
207                         enum lambda = "(captures) { " ~ funcCall ~ " }";
208 
209                     }
210 
211                     //e.g. mymod.myfunc:13
212                     enum source = `"` ~ mod ~ "." ~ member ~ `:` ~ getLineNumber!(mixin(member))
213                             .to!string ~ `"`;
214 
215                     //e.g. cucumberSteps ~= CucumberStep((in string[] cs) { myfunc(); }, r"foobar",
216                     //                                   ++id, "foo.bar:3");
217                     enum mixinStr = `cucumberSteps ~= CucumberStep(` ~ lambda
218                         ~ `, ` ~ reg ~ `, ++id, ` ~ source ~ `);`;
219 
220                     mixin(mixinStr);
221                 }
222             }
223         }
224     }
225 
226     return cucumberSteps;
227 }
228 
229 /**
230  * Normally this struct wouldn't exist and I'd use a delegate.
231  * But for the wire protocol I need access to the captures
232  * array so it's a struct.
233  */
234 struct MatchResult
235 {
236     ///
237     CucumberStepFunction func;
238     ///
239     CucumberStepFunctionWithDocString funcWithDocString;
240     ///
241     CucumberStepFunctionWithDataTable funcWithDataTable;
242     ///
243     const(string)[] captures;
244     ///
245     int id;
246     ///
247     string regex;
248     ///
249     string source;
250 
251     ///
252     this(in CucumberStepFunction func, in CucumberStepFunctionWithDocString funcWithDocString,
253             in CucumberStepFunctionWithDataTable funcWithDataTable,
254             in string[] captures, in int id, in string regex, in string source)
255     {
256         this.func = func;
257         this.funcWithDocString = funcWithDocString;
258         this.funcWithDataTable = funcWithDataTable;
259         this.captures = captures;
260         this.id = id;
261         this.regex = regex;
262         this.source = source;
263     }
264 
265     ///
266     void opCall(DocString docString) const
267     {
268         if (funcWithDocString is null)
269             throw new Exception("MatchResult with null function");
270         funcWithDocString(captures, docString);
271     }
272 
273     ///
274     void opCall(DataTable dataTable) const
275     {
276         if (funcWithDataTable is null)
277             throw new Exception("MatchResult with null function");
278         funcWithDataTable(captures, dataTable);
279     }
280 
281     ///
282     void opCall() const
283     {
284         if (func is null)
285             throw new Exception("MatchResult with null function");
286         func(captures);
287     }
288 
289     ///
290     bool opCast(T : bool)() const
291     {
292         return !(func is null && funcWithDocString is null && funcWithDataTable is null);
293     }
294 }
295 
296 /**
297  * Finds the match to a step string. Checks all steps and loops
298  * over to see which one has a matching regex. Steps are found
299  * at compile-time.
300  */
301 MatchResult findMatch(ModuleNames...)(string step_str)
302 {
303     step_str = stripCucumberKeywords(step_str);
304     enum steps = findSteps!ModuleNames;
305     foreach (step; steps)
306     {
307         auto m = step_str.match(step.regex);
308         if (m)
309         {
310             import std.array : array;
311 
312             return MatchResult(step.func, step.funcWithDocString, step.funcWithDataTable,
313                     m.captures.array, step.id, step.regexString, step.source);
314         }
315     }
316 
317     return MatchResult();
318 }
319 
320 /**
321  * Counts the number of parentheses pairs in a string known
322  * at compile-time
323  */
324 int countParenPairs(string reg)()
325 {
326     int intCount(in string haystack, in string needle)
327     {
328         import std.algorithm : count;
329 
330         return cast(int) haystack.count(needle);
331     }
332 
333     //no need to count matching closing parens since it won't be a valid
334     //regexp and will throw an exception at runtime anyway
335     return intCount(reg, "(") - intCount(reg, r"\(") - intCount(reg, r"(?:");
336 }
337 
338 unittest
339 {
340     static assert(countParenPairs!r"" == 0);
341     static assert(countParenPairs!r"foo" == 0);
342     static assert(countParenPairs!r"\(\)" == 0);
343     static assert(countParenPairs!r"()" == 1);
344     static assert(countParenPairs!r"()\(\)" == 1);
345     static assert(countParenPairs!r"\(\)()" == 1);
346     static assert(countParenPairs!r"()\(\)()" == 2);
347     static assert(countParenPairs!r"(foo).+\(oh noes\).+(bar)" == 2);
348     static assert(countParenPairs!r"(.+).+(?:oh noes).+" == 1);
349 }
350 
351 /**
352  * Returns an array of string mixins to convert each type
353  * from a string
354  */
355 auto conversionsFromString(Types...)()
356 {
357     string[] convs;
358     foreach (T; Types)
359     {
360         static if (isSomeString!T)
361         {
362             convs ~= "";
363         }
364         else
365         {
366             convs ~= ".to!" ~ Unqual!T.stringof;
367         }
368     }
369     return convs;
370 }
371 
372 unittest
373 {
374     static assert(conversionsFromString!(int, string) == [".to!int", ""]);
375     static assert(conversionsFromString!(string, double) == ["", ".to!double"]);
376 }
377 
378 /**
379  * Comma separated argument list for calls to variadic functions
380  * associated with regexen. Meant to be used with mixin to
381  * generate code.
382  */
383 string argsString(string reg, alias func, string extraArg = "")()
384 {
385     import std.string : empty;
386 
387     enum convs = conversionsFromString!(ParameterTypeTuple!func);
388     enum numCaptures = countParenPairs!reg;
389     static assert(convs.length == numCaptures + (extraArg.empty ? 0 : 1),
390             text("Wrong length for ", convs, ", should be ", numCaptures));
391 
392     string[] args;
393     foreach (i; 0 .. numCaptures)
394     {
395         args ~= "captures[" ~ (i + 1).to!string ~ "]" ~ convs[i];
396     }
397     if (!extraArg.empty)
398     {
399         args ~= extraArg;
400     }
401 
402     import std.array : join;
403 
404     return args.join(", ");
405 }
406 
407 ///
408 string argsStringWithParens(string reg, alias func)()
409 {
410     return "(" ~ argsString!(reg, func) ~ ")";
411 }
412 
413 unittest
414 {
415     void func()
416     {
417     }
418 
419     static assert(argsString!(r"", func) == "");
420 
421     void func_is(int, string)
422     {
423     }
424 
425     static assert(argsString!(r"(foo)...(bar)", func_is) == "captures[1].to!int, captures[2]");
426 
427     void func_cis(in int, in string)
428     {
429     }
430 
431     static assert(argsString!(r"(foo)...(bar)", func_cis) == "captures[1].to!int, captures[2]");
432 
433     void func_si(string, int)
434     {
435     }
436 
437     static assert(argsString!(r"(foo)...(bar)", func_si) == "captures[1], captures[2].to!int");
438 
439     void func_dd(double, double)
440     {
441     }
442 
443     static assert(argsString!(r"(foo)...(bar)",
444             func_dd) == "captures[1].to!double, captures[2].to!double");
445 
446     void funcWithExtraArg(int, DataTable)
447     {
448     }
449 
450     static assert(argsString!(r"(foo)...", funcWithExtraArg,
451             "extra") == "captures[1].to!int, extra");
452 }
453 
454 /**
455  * This function is necessary due to the extreme "metaness" of
456  * this project. It returns a string that is meant to be consumed
457  * by mixin to generate code. Since the strings being fed to
458  * the system at compile-time are generally going to be
459  * "raw" strings, they'll either be r"" or `` style D strings.
460  * If the regex requires the '"' character they'll use ``, and
461  * vice-versa. When generating code, I opted for r"" strings.
462  * This would cause a compilation error if the regex has the
463  * quote character in it. So this function returns an escaped
464  * version that can be safely used.
465  */
466 string rawStringMixin(in string str)
467 {
468     import std.array : replace;
469 
470     return "r\"" ~ str.replace(`"`, "\" ~ `\"` ~ r\"") ~ "\"";
471 }
472 
473 unittest
474 {
475     static assert(rawStringMixin(
476             `Sharks with "lasers" yo.`) == "r\"Sharks with \" ~ `\"` ~ r\"lasers\" ~ `\"` ~ r\" yo.\"");
477     static assert(rawStringMixin(`foo bar baz`) == "r\"foo bar baz\"");
478 }