Builders
Prerequisites: Closures in depth, Working with XML
A builder turns nested method calls and closures into a structure:
XML, HTML, JSON, files, object graphs, or anything else. You've used
MarkupBuilder and JsonBuilder; here are more, and how to write your
own.
HTML with MarkupBuilder
import groovy.xml.MarkupBuilder
def products = [[name: 'Keyboard', price: 49], [name: 'Mouse', price: 19]]
def out = new StringWriter()
new MarkupBuilder(out).html {
body {
h1 'Products'
ul(class: 'products') {
products.each { p ->
li { b p.name; mkp.yield " — \$${p.price}" }
}
}
}
}
println out
<html>
<body>
<h1>Products</h1>
<ul class='products'>
<li>
<b>Keyboard</b> — $49
</li>
<li>
<b>Mouse</b> — $19
</li>
</ul>
</body>
</html>
mkp.yield inserts text (escaped) next to other elements.
FileTreeBuilder
def root = new File('demo-app')
root.mkdirs()
new FileTreeBuilder(root).with {
'README.md'('# Demo app')
src {
main {
'App.groovy'('println "hello"')
}
}
'build.gradle'("plugins { id 'groovy' }")
}
def files = []
root.eachFileRecurse { f ->
if (f.file) files << root.toPath().relativize(f.toPath()).toString().replace('\\', '/')
}
println files.sort()
println new File(root, 'src/main/App.groovy').text
root.deleteDir()
[README.md, build.gradle, src/main/App.groovy]
println "hello"
Useful for generating project skeletons and test fixtures.
Writing your own builder
A builder is just an object whose methods take closures, with the closure's delegate pointed at something that collects the calls:
class RecipeBuilder {
String title
List<String> ingredients = []
List<String> steps = []
void title(String t) { title = t }
void ingredient(String qty, String what) { ingredients << "$qty $what" }
void step(String text) { steps << text }
static RecipeBuilder recipe(@DelegatesTo(RecipeBuilder) Closure spec) {
def b = new RecipeBuilder()
b.with(spec)
b
}
String toString() {
def lines = ["== $title ==", *ingredients.collect { "- $it" }]
steps.eachWithIndex { s, i -> lines << "${i + 1}. $s" }
lines.join('\n')
}
}
println RecipeBuilder.recipe {
title 'Pancakes'
ingredient '200 g', 'flour'
ingredient '2', 'eggs'
ingredient '300 ml', 'milk'
step 'Whisk everything together'
step 'Fry in a hot pan'
}
== Pancakes ==
- 200 g flour
- 2 eggs
- 300 ml milk
1. Whisk everything together
2. Fry in a hot pan
with(closure)runs the closure with the object as its delegate.@DelegatesTo(RecipeBuilder)tells the IDE and type checker what the delegate will be, so completion works inside the block.*listinside a list literal spreads its items.
Level 3's DSL lesson builds on this with command chains and
methodMissing for builders that accept any name.