Reading — step 1 of 5
Learn
~1 min readTraits and DSL Composition
For Gradle-style DSLs, you want IDE autocompletion and compile-time validation inside the closure. The @DelegatesTo annotation tells Groovy where to route undefined names.
import groovy.lang.DelegatesTo
class HtmlBuilder {
private List parts = []
void p(String text) { parts << "<p>${text}</p>" }
void h1(String text) { parts << "<h1>${text}</h1>" }
String build() { "<html><body>${parts.join('')}</body></html>" }
}
String html(@DelegatesTo(HtmlBuilder) Closure body) {
def builder = new HtmlBuilder()
body.delegate = builder
body.resolveStrategy = Closure.DELEGATE_FIRST
body.call()
return builder.build()
}
def result = html {
h1 "Welcome"
p "This is a paragraph"
p "Another paragraph"
}
println result
The @DelegatesTo(HtmlBuilder) tells:
- IDE: provide autocomplete for HtmlBuilder methods inside the block
- Static checker: verify these are valid methods
- Runtime: actually delegates the calls (the real behavior)
Strategies for inner DSLs:
DELEGATE_FIRST— delegate methods winOWNER_FIRST(default) — calling code's methods winDELEGATE_ONLY/OWNER_ONLY— exclusive
Type information for nested closures:
String section(@DelegatesTo(SectionBuilder) Closure body) { ... }
String page(@DelegatesTo(PageBuilder) Closure body) {
def b = new PageBuilder()
body.delegate = b
body.call()
b.build()
}
page {
section { ... } // IDE knows this 'section' delegates to SectionBuilder
}
This is how Gradle's build files work:
plugins {
id 'java' // PluginsBlock methods
}
dependencies {
implementation 'commons-io:commons-io:2.11' // DependencyHandler methods
}
Each top-level closure delegates to a different builder type — IDE shows the right autocomplete based on which block you're in.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…