Skip to content
AgileLogics
6 min read

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.
  • *list inside 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.