Wednesday, November 14, 2007

Groovy Style "builders" in Suneido

One of the neat features in Groovy is its "builders". For example, using an XML markup builder:
builder.invoices
{
invoice(number: 1234)
{
item(type: 'part')
{ product(name: 'widget', cost: 100) }
}
}
which produces:
<invoices>
<invoice number="1234">
<item type="part">
<product name="widget" cost="100" />
</item>
</invoice>
</invoices>
The builder doesn't actually have methods for "invoice", "part", etc. Instead, dynamic language "tricks" are used to catch "unknown" method calls.

It made me wonder how close I could come to this with Suneido. Here's what I came up with:
builder.invoices()
{
.invoice(number: 1234)
{
.item(type: 'part')
{ .product(name: 'widget', cost: 100) }
}
}
The main difference is that Suneido requires '.' on method calls. Otherwise it's pretty much identical. One thing the Groovy XML builder doesn't handle is tag contents containing a mixture of text and tags. I handled this in Suneido with a special '_' method. For example:
builder.p()
{
._('start ')
.b() { 'middle' }
._(' end')
}
which produces:
<p>start <b>middle</b> end</p>
Here is the entire implementation of a Suneido XmlBuilder:
class
{
New()
{ .s = '' }
Default(@args)
{
.s $= '<' $ args[0]
for m in args.Members(named:)
if m isnt #block
.s $= ' ' $ m $ '="' $ args[m] $ '"'
if args.Member?(#block)
{
.s $= '>'
result = .Eval2(args.block)
if result.Size() is 1
.s $= result[0]
.s $= '</' $ args[0] $ '>'
}
else
.s $= ' />'
return
}
_(s)
{ .s $= s; return }
ToString()
{ return .s }
}
One minor problem with the Suneido version is that certain methods are "built in" (e.g. Size) and therefore can't be used in the builder. [This is a result of trying to make class instances behave the same as generic containers. I'm starting to think this was a mistake, but I'm not sure how to go about changing something so fundamental.]

I had to make to make a slight fix to Suneido to make this work. The approach I used was to use instance.Eval(function) to evaluate the blocks in the "context" of the builder. But I found that Eval didn't work with blocks (only functions). Luckily it was easy to fix. (Actually, I'm using Eval2 which returns the result inside an object so you can determine if there was a return value or not.)

3 comments:

Graeme Rocher said...

Nice post. Note that MarkupBuilder in Groovy doesn't support mixed content, but StreamingMarkupBuilder (a similar builder that lazily streams XML ) does.

paulk said...

MarkupBuilder also supports mixed content.

paulk said...

MarkupBuilder also supports simple mixed content in Groovy 1.1.