Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save deanwampler/161702 to your computer and use it in GitHub Desktop.
Save deanwampler/161702 to your computer and use it in GitHub Desktop.
/**
* http://gist.github.com/161702.
* Start with this Groovy example, http://gist.github.com/160492
* and Scala variants suggested by others, http://gist.github.com/161159
* and http://gist.github.com/162389
*
* Here is what I came up with, but it's somewhat different. The others assume
* that Cucumber will parse the regex's and invoke the anonymous function
* specified with the Given declaration. The problem is that it's difficult to
* define a generic function signature.
*
* Instead, I assume that Scala will be used parse strings with the regexs, using
* the Given.eval(...) function defined below. Then, the anonymous function
* specified by the user is actually a PartialFunction. "PFs" are "defined" over
* a subset of the domain of the single argument's type. For example, a PF that
* takes an Int might only be defined for positive even Ints. Usually, PFs are
* written as case expressions; the surrounding "match" clause is supplied by
* the compiler. In this case, I've assumed that all the PFs will take a Product
* argument, which is the parent type of ProductN types, for N = 1 - 22, which
* are the parents of correspondingTupleN types. Hence, my examples use
* case (x1, x2, ..., xN) => ...
* expressions.
*/
import scala.util.matching._
import scala.util.matching.Regex._
import scala.collection.mutable._
/**
* Constructor takes two argument lists, each with a single argument. This makes
* the syntax nice, as in the examples below.
*/
class Given(name: String)(body: PartialFunction[Product,Unit]) {
val regex = name.r
/**
* See if the regex matches. If so, see if the ParialFunction applies. If so,
* call it on the groups extracted from the string by the regex.
* The Regex and MatchIterator APIs don't work like normal Scala collections,
* etc., so we have to hack around...
*/
def apply(str: String): Boolean = {
val iter = regex.findAllIn(str)
if (iter.hasNext == false) return false
val values = toTuple(iter)
if (body.isDefinedAt(values)) { body(values); true } else false
}
/**
* There's no convenient way to map an arbitrarily-sized collection to a
* tuple (up to Tuple22, of course), so we do it "manually". (We use tuples to
* provide the convenient syntax used in the examples below.)
*/
protected def toTuple(iter: MatchIterator) = {
val list = iter.subgroups
list.size match {
case 0 => Tuple1(iter.matched) // No capture groups; return whole string.
case 1 => Tuple1(list(0))
case 2 => Tuple2(list(0), list(1))
case 3 => Tuple3(list(0), list(1), list(2))
case 4 => Tuple4(list(0), list(1), list(2), list(3))
case n => throw new RuntimeException(
n + " > 4 arguments not yet supported, but you could add them easily...")
}
}
}
/**
* "Companion object", used to track the defined Givens and find the right one for
* a particular input string, using "eval". Also provides the typical "apply"
* factory method for creating Given instances.
*/
object Given {
val givens = new ListBuffer[Given]
def apply(name: String)(body: PartialFunction[Product,Unit]): Given = {
givens prepend new Given(name)(body)
givens first // return new Given
}
/**
* Tests if any Givens match the string and if found, invokes the Given's
* "action" implicitly.
*/
def eval(test: String) = givens.exists(g => g(test)) ||
{ println("Nothing matched string: " + test); false }
}
// Example #1
Given("I ate (\\d+) cukes for (\\w+) and I feel (.+)") {
case (cukes, meal, state) =>
println("You ate " + cukes + " cukes during " + meal + " and you feel " + state)
}
// Example #2
Given("I have (\\d+) cukes in my (.+)") {
case (cukes, where) => println("You have " + cukes + " cukes in your " + where)
}
// Example #3
// Use "Tuple1(x)" rather than "(x)", because the latter is ambiguious; is it a
// tuple or just parentheses around an expression?
Given("my name is (.+)") {
case Tuple1(name) => println("Your name is " + name)
}
// Example #4
// No capture groups, so will match the whole string verbatim.
Given("This is a fixed string") {
case Tuple1(str) => println("The whole string is: " + str)
}
// Try the three options:
Given.eval("I ate 7 cukes for lunch and I feel bad")
Given.eval("I have 5 cukes in my body")
Given.eval("I have 6 cukes in my other body")
Given.eval("my name is Dean")
Given.eval("my name is Aslak")
Given.eval("my name is ")
Given.eval("This is a fixed string")
// Output:
// => You ate 7 cukes during lunch and you feel bad
// => You have 5 cukes in your body
// => You have 6 cukes in your other body
// => Your name is Dean
// => Your name is Aslak
// => Nothing matched string: my name is
// => The whole string is: This is a fixed string
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment