Inspired by this question on stackoverflow, I decided to create a utility class that allowed me to determine generically what calls a closure makes (without actually letting it make any calls). This lets me see what it’s trying to do before letting it actually do it.
It works by overriding the delegate of the closure (a delegate intercepts all method and property get/set calls that have not already been dealt with). It assigns it to an instance of the ClosureInterrogator which implements the propertyMissing and methodMissing methods. Any property or set method that is called gets saved in a map that is returned to the caller.
class ClosureInterrogator {
private Map closureValueMap = [:]
static Map extractValuesFromClosure(Closure closure) {
def interrogator = new ClosureInterrogator(closure)
return interrogator.closureValueMap
}
private ClosureInterrogator(Closure closure) {
def oldResolveStrategy = closure.getResolveStrategy()
def oldDelegate = closure.getDelegate()
closure.delegate = this
closure.resolveStrategy = Closure.DELEGATE_FIRST
try {
closure()
} finally {
closure.setDelegate(oldDelegate)
closure.setResolveStrategy(oldResolveStrategy)
}
}
// property getter
def propertyMissing(String name) {
return closureValueMap[name]
}
// property setter
def propertyMissing(String name, value) {
closureValueMap[name] = value
}
def methodMissing(String name, args) {
if (args.size() == 1) {
closureValueMap[name] = args[0]
} else {
closureValueMap[name] = args
}
}
}
This technique is the basis for all groovy mocking techniques and libraries, it’s also the main technique used to create a DSL with groovy.
Here is a sample class that has a closure (“something”) that we want to extract the values from.
class SomeClass {
static something = {
key1 "value1" // calls methodMissing("key1", ["value1"])
key2("value2") // calls methodMissing("key2", ["value2"])
key3 = "value3" // calls propertyMissing("key3", "value3")
key4 "foo", "bar", "baz" // calls methodMissing("key4", ["foo","bar","baz"])
}
}
If we call “extractValuesFromClosure” on the “something” closure, we’ll get back a map that has all of the values we want in it.
def closureValueMap = ClosureInterrogator.extractValuesFromClosure(new SomeClass().something)
assert "value1" == closureValueMap."key1" // calls propertyMissing("key1")
assert "value2" == closureValueMap."key2" // calls propertyMissing("key2")
assert "value3" == closureValueMap."key3" // calls propertyMissing("key3")
assert ["foo", "bar", "baz"] == closureValueMap."key4" // calls propertyMissing("key4")
This class will work with any type of closure, such as this closure used by the grails mail plugin to send mail.
def mailClosure = {
to "fred@g2one.com","ginger@g2one.com"
from "john@g2one.com"
cc "marge@g2one.com", "ed@g2one.com"
bcc "joe@g2one.com"
subject "Hello John"
body 'this is some text'
}
def results = ClosureInterrogator.extractValuesFromClosure(mailClosure)
println results
// prints:
// [to:[fred@g2one.com, ginger@g2one.com], from:john@g2one.com, cc:[marge@g2one.com, ed@g2one.com], bcc:joe@g2one.com, subject:Hello John, body:this is some text]
assert results.to.every { it.endsWith("g2one.com") }
If we wanted to assert that within a test environment, we’re only sending e-mail to a specific domain, this would be one technique to assert it before we actually send the e-mail off.
ClosureInterrogator is generic enough that it could also be used on a number of Grails/GORM domain closures such as “constraints” and “mapping”.
Thanks for the tip!
Just one thing: I would wrap calling closure() in a try {} finally {} statement, so that the delegate and resolve strategy are reset, even if an exception was thrown. Another way would be to clone() the closure, as described by Peter Ledbrook in his blog: http://www.cacoethes.co.uk/blog/groovyandgrails/supporting-reloading-in-your-grails-plugins
@Wolfgang – good suggestion about the try/finally (cloning would also work). I’ve modified the post to include it.
[...] This post was mentioned on Twitter by Debasish Ghosh, Dmitriy Kopylenko, HamletDRC, Ted Naleid, Zak Jacobson and others. Zak Jacobson said: @bradfordcross Look familiar? how to interrogate Groovy closure values without evaluating the closure: http://is.gd/6YVKl (via @TedNaleid) [...]
[...] Interrogating Arbitrary Groovy Closures for Values – Ted Naleid [...]
Thanks Ted for this wonderful writeup around interrogating closures. Exactly what I needed tonight working on some unit tests in Grails.
– chris –