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 }