Groovy Goodness: @DelegatesTo For Type Checking DSL
Groovy 2.1 introduced the @DelegatesTo
annotation. With this annotation we can document a method and tell which class is responsible for executing the code we pass into the method. If we use @TypeChecked
or @CompileStatic
then the static type checker of the compiler will use this information to check at compile-time if the code is correct. And finally this annotation allows an IDE to give extra support like code completion.
Suppose we have the following class Reservation
with the method submit()
. The method accepts a closure with methods that need to be applied with the instance of the Reservation
class. This is a very common pattern for writing simple DSLs in Groovy.
class Reservation {
private Date date
private String event
private String attendee
void date(final Date date) { this.date = date }
void event(final String event) { this.event = event }
void attendee(final String attendee) { this.attendee = attendee }
/** Submit a reservation.
* @param config Configuration for reservation, invoking method class on Reservation.
*/
static void submit(final Closure config) {
final Reservation reservation = new Reservation()
reservation.with config
}
}
class Event {
/** Use Reservation configuration DSL to submit a reservation. */
void submitReservation() {
Reservation.submit {
date Date.parse('yyyyMMdd', '20130522')
event 'Gr8Conf'
attendee 'mrhaki'
reserved true
}
}
}
final event = new Event()
event.submitReservation()
When we look at the code we might already see there is an error. In the Event.submitReservation()
method we have the line reserved true
, which will try to invoke the reserve()
method of the Reservation
class. But that method is not defined. When we run the application we get the expected error:
Exception thrown
May 28, 2013 6:58:36 AM org.codehaus.groovy.runtime.StackTraceUtils sanitize
WARNING: Sanitizing stacktrace:
groovy.lang.MissingMethodException: No signature of method: Reservation.reserved() is applicable for argument types: (java.lang.Boolean) values: [true]
To get an error for this line at compile-time we must add some annotation so the Groovy compiler can do static type checking on our code. We change the code and get the following sample:
import groovy.transform.*
class Reservation {
private Date date
private String event
private String attendee
void date(final Date date) { this.date = date }
void event(final String event) { this.event = event }
void attendee(final String attendee) { this.attendee = attendee }
/** Submit a reservation.
* @param config Configuration for reservation, invoking method class on Reservation.
*/
static void submit(@DelegatesTo(Reservation) final Closure config) {
final Reservation reservation = new Reservation()
reservation.with config
}
}
@TypeChecked
// @CompileStatic - will also do static type checking
class Event {
/** Use Reservation configuration DSL to submit a reservation. */
void submitReservation() {
Reservation.submit {
date Date.parse('yyyyMMdd', '20130522')
event 'Gr8Conf'
attendee 'mrhaki'
reserved true
}
}
}
final event = new Event()
event.submitReservation()
When the code is compiled we immediately get a compilation error:
1 compilation error:
[Static type checking] - Cannot find matching method Event#reserved(boolean). Please check if the declared type is right and if the method exists.
at line: 26, column: 13
Wow, this useful! We find errors in our DSL before the code is run.
When we create this code in an IDE like IntelliJ IDEA we also get code completion in the Reservation.submit()
method invocation in the Event
class. The following screenshot shows code completion and a red font for reserved
to indicate the compilation error.
Code written in Groovy 2.1.3