Groovy Dice provides a powerful way to extend - and even replace - the built-in API by using a simple plugin architecture.
Here goes a simple plugin that adds a method to dice rolling command objects:
class HitchhikersGuideToTheGalaxy { /* this closure is called during the Groovy Dice initialization */ def dynamicMethods = { api -> api.add(method:'is_the_answer_to_life_the_universe_and_everything') { dice -> dice.allDice.sum() == 42 } } } /* register the plugin at the initialization step */ def config = new GroovyDice() config.pluginManager.register(new HitchhikersGuideToTheGalaxy()) config.initialize() 5.d10.is_the_answer_to_life_the_universe_and_everything
Every plugin must provide a method called dynamicMethods. This method receives an instance of GroovyDiceAPI which is used to register new methods to the API.
In this example we added a method to check whether the sum of all dice is equals to 42.
At this time, the only lifecycle event that a plugin can listen to is the onInitialize event, which is invoked when the initialize() method is called on the corresponding GroovyDice object:
class MyPlugin { GroovyDice config def onInitialize = { config -> /* we can save the reference to 'config' */ this.config = config } def dynamicMethods = { api -> // ... } } def config = new GroovyDice() config.pluginManager.register(new MyPlugin()) /* this triggers the 'onInitialize' event on the registered plugins */ config.initialize()
Your plugins don't have to have such method to work. The only practical reason you would use this feature is because your plugin needs the GroovyDice object to do its job.
Several plugins can work together to provide an implementation of the same method. An example:
class FirstPlugin { def dynamicMethods = { api -> api.add(method:'looks_like', to:api.numberClasses) { num, obj -> if (obj instanceof Date) { return "First: $num - $obj" } } } } class SecondPlugin { def dynamicMethods = { api -> api.add(method:'looks_like', to:api.numberClasses) { num, obj -> if (obj instanceof Integer) { return "Second: $num - $obj" } } } } def config = new GroovyDice() config.pluginManager.register(new FirstPlugin()) config.pluginManager.register(new SecondPlugin()) config.initialize() println 10.looks_like(20) // prints "Second: 10 - 20" println 10.looks_like(new Date()) // prints "First: 10 - (time)" println 10.looks_like{ println 'A closure!' } // no plugin can handle a closure!
In this example we have two plugins that "listens" to the same method call, but the first plugin only comes into scene when the given object is a Date and the second plugin only do it's job when the given object is an Integer. To give every plugin a chance to execute, Groovy Dice will try to call every closure registered under that name.
Since the plugin registration order is relevant, Groovy Dice calls the closure registered by FirstPlugin, passing to it the called Integer instance and the given parameter (10 and 20, respectively). Since the parameter is an Integer, the closure returns nothing and then the other registered closures are called.
This process continues until someone returns something valid. In this example, the closure provided by SecondPlugin returns a not null value; then this process stops even if there are other closures that can handle the call. If no registered closure can handle it, Groovy Dice will raise an exception.
If you want to intercept calls to methods that match a particular pattern, you are allowed to use regular expressions as method names:
class HelloPlugin { def dynamicMethods = { api -> api.add(method:/say.*/, to:Integer) { number -> 'Hello!' } } } /* register the plugin at the initialization step */ 10.say 10.sayHello 10.saySomething
This feature is pretty useful when you want to intercept a method which you don't know exactly the name. If you want to know the method name that has activated the closure, its first parameter should be explicitely defined as String:
class HelloPlugin { def dynamicMethods = { api -> api.add(method:/say.*/, to:Integer) { String name, number -> "$name - $number" } } } /* register the plugin at the initialization step */ 10.say // prints "say - 10" 10.sayHello // prints "sayHello - 10" 10.saySomething // prints "saySomething - 10"
Doing this, the closure will receive the method name and the called object at the first and second parameters respectively.
Most of Groovy Dice's features were implemented as plugins, making your life much easier when you need to change the default plugin stack.
The following script shows some examples on how to manipulate the plugin stack:
def config = new GroovyDice() def manager = config.pluginManager // default plugin stack already defined def plugin = new MyPlugin() // some plugin manager.register(plugin) // register plugin manager.unregister(plugin) // unregister plugin manager.unregister(MyPlugin) // unregister all MyPlugin instances manager.unregister() // clear the whole plugin stack