Plugins

Groovy Dice provides a powerful way to extend - and even replace - the built-in API by using a simple plugin architecture.

Creating plugins

A simple example

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.

Plugin lifecycle events

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 implement the same method collaboratively

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.

Plugins can use regex patterns as method names

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.

Manipulating the built-in plugin stack

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

Limitations

  1. A plugin can call methods registered by other plugins, but you have to be careful when dealing with such dependencies. For example, let's say you created a plugin A that depends on the plugin B. Then, you have to register manually both A and B before the initialization step. Groovy Dice won't manage situations like that by itself;
  2. You can't add methods to any class you want; you are restricted to numbers and dice rolling command classes.