Search

Dark theme | Light theme

May 5, 2014

Groovy Goodness: Use Builder AST Transformation for Fluent API

Since Groovy 2.3 we can easily create a fluent API for our classes with the @Builder AST transformation. We can apply the annotation to our classes and the resulting class file will have all the necessary methods to support a fluent API. We can customize how the fluent API is generated with different annotation parameters. In Groovy code we already can use the with method to have a clean way to set property values or use the named constructor arguments. But if our classes need to be used from Java it is nice to give the Java developers a fluent API for our Groovy classes.

In the following sample we apply the @Builder annotation to a simple class Message with some properties. We leave everything to the default settings and then the resulting Message class file will have a new builder method that return an internal helper class we can use to set our properties. For each property their is a new method with the name of the property so we can set a value. And finally our class contains a build that will return a new instance of the Message class with the correct values for the properties.

import groovy.transform.builder.Builder

@Builder
class Message {
    String from, to, subject, body
}

def message = Message
        .builder()  // New internal helper class.
        .from('mrhaki@mrhaki.com')  // Method per property.
        .to('mail@host.nl')
        .subject('Sample mail')
        .body('Groovy rocks!')
        .build()  // Create instance of Message

assert message.body == 'Groovy rocks!'
assert message.from == 'mrhaki@mrhaki.com'
assert message.subject == 'Sample mail'
assert message.to == 'mail@host.nl'

If we want to change the names of the builder and build methods we can use the annotation parameters builderMethodName and buildMethodName:

import groovy.transform.builder.Builder

@Builder(builderMethodName = 'initiator', buildMethodName = 'create')
class Message {
    String from, to, subject, body
}

def message = Message.initiator()
        .from('mrhaki@mrhaki.com')
        .body('Groovy rocks!')
        .create()

assert message.body == 'Groovy rocks!'
assert message.from == 'mrhaki@mrhaki.com'

We see that for each property a corresponding method is generated. We can also customize the prefix for the generated method name with the annotation parameter prefix. In the following sample we define the prefix assign for the method names:

import groovy.transform.builder.Builder

@Builder(prefix = 'assign')
class Message {
    String from, to, subject, body
}

def message = Message.builder()
        .assignFrom('mrhaki@mrhaki.com')
        .assignBody('Groovy rocks!')
        .build()

assert message.body == 'Groovy rocks!'
assert message.from == 'mrhaki@mrhaki.com'

Finally we can also include and exclude properties to need to be included or excluded from our fluent API. We use the annotation parameters includes and excludes to define the names of the properties. This can be a list or a comma separated list of names.

import groovy.transform.builder.Builder

@Builder(excludes = 'body' /* or includes = 'from,to,subject' */)
class Message {
    String from, to, subject, body
}

def message = Message.builder()
        .from('mrhaki@mrhaki.com')
        .to('mail@host.nl')
        .subject('Groovy 2.3 is released')
        .build()

assert message.from == 'mrhaki@mrhaki.com'
assert message.subject == 'Groovy 2.3 is released'

try {
    message = Message.builder().body('Groovy rocks!').build()
} catch (MissingMethodException e) {
    assert e.message.readLines().first() ==
            'No signature of method: static Message.body() is applicable for argument types: (java.lang.String) values: [Groovy rocks!]'
}

The @Builder AST transformation also checks if the @Canonical AST transformation is applied to a class. Any included or excluded properties defined in the @Canonical transformation are also included or excluded for the generated builder code.

We can define the SimpleStrategy strategy with the builderStrategy annotation parameter. Then the generated class will not have a separate inner helper builder class and build method. The default prefix is set to set, but we can change that if we want to:

import groovy.transform.builder.Builder
import groovy.transform.builder.SimpleStrategy

@Builder(builderStrategy = SimpleStrategy, prefix = 'assign')
class Message {
    String from, to, subject, body
}

def message = new Message()
        .assignFrom('mrhaki@mrhaki.com')  // Method per property.
        .assignTo('mail@host.nl')
        .assignSubject('Sample mail')
        .assignBody('Groovy rocks!')

assert message.body == 'Groovy rocks!'
assert message.from == 'mrhaki@mrhaki.com'
assert message.subject == 'Sample mail'
assert message.to == 'mail@host.nl'

We will see other feature of the @Builder annotation in future blog posts.

Code written with Groovy 2.3.