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 }