Coffee DSL Redone With Meta-Programming

January 7, 2008 - 4 minute read -
dsl meta-programming

In a previous post I wrote about DSLs as Jargon. I implemented a simple Coffee DSL that would allow code to parse an order written by a human and turn it into a domain model. I used a fairly basic method_missing structure to capture the values.

There's a much better way to do it in Ruby with meta-programming. Meta-programming allows you to write code to write code. You program your programming. In this case we can create the syntax of Coffee using a meta-programming technique.

dsl_attr :size, %w(venti grande tall)

This is us programming the class to say: "If someone calls a method venti, grande, or tall on our object they mean that they are telling us the size of the coffee, so store that value as the size". So now we can write our Coffee class like this:

# CoffeeDSL.rb
# This is the input from the user, likely read from a file
# or input through a user interface of some sort
CoffeeInput = "venti nonfat whip latte"
class Coffee
    dsl_attr :size, %w(venti grande tall)
    dsl_attr :whipped, %w(whip nowhip)
    dsl_attr :caffinated, %w(caf decaf halfcaf)
    dsl_attr :type, %w(regular latte cappachino)
    dsl_attr :milks, %w(milk nonfat soy)
    def order
        params = ''
        params += milks + ' ' if milks?
        params += caffinated + ' ' if caffinated?
        params += whipped + ' ' if whipped?
        print "Ordering coffee: #{size} #{params}#{type}\n"
    end
    def load
        # turn one line into multi-line "method calls"
        cleaned = CoffeeInput.gsub(/\s+/, "\n")
        self.instance_eval(cleaned)
    end
end

We are essentially configuring the class in code. We could add extra values as well, such as a default value, required validation, any number of things. We then just need to implement the dsl_attr using meta-programming. That can be done in the Module in Ruby which makes that available to all classes in the system.

class Module
    def dsl_attr(param_name, values)
        attr param_name
        class_eval "def #{param_name}?; @#{param_name}; end"
        values.each do |val|
            define_method("#{val}") do
                instance_eval %{
                    @#{param_name} = '#{val}'
                }
            end
      end
    end
end

Now when you run the code it captures all of the values that are parsed from the input and puts them into your object as meaningful values.

c = Coffee.new
c.load
c.order

I did the same DSL in Groovy and thought I could attempt to do it more justice using meta-programming as well. In Groovy, meta-programming is done with the ExpandoMetaClass - no, I didn't make that up. Each Class has a metaClass property that gets you access to that types' ExpandoMetaClass instance. You can then add properties and methods and whatnot to it. This has the effect of making the properties or methods callable on an instance of that type.

ExpandoMetaClass.enableGlobally()   // have to do this to get inheritance of dslAttr
Object.metaClass.dslAttr << {String param_name, values ->
    def clazz = delegate
    clazz.metaClass."${param_name}" = null
    values.each() { val ->
        clazz.metaClass."${val}" << {-> clazz."${param_name}" = "${val}" }
    }
}
class Coffee {
    def Coffee() {
        dslAttr("size", ['venti', 'tall', 'grande'])
        dslAttr("whipped", ['whip', 'nowhip'])
        dslAttr("caffinated", ['caf', 'decaf', 'halfcaf'])
        dslAttr("type", ['regular', 'latte', 'cappachino'])
        dslAttr("milks", ['milk', 'nonfat', 'soy'])
    }
    def order() {
        def params = ''
        if (null != getMilks()) params += "${getMilks()} "
        if (null != getCaffinated()) params += "${getCaffinated()} "
        if (null != getWhipped()) params += "${getWhipped()} "
        println "Ordering coffee: ${getSize()} ${params}${getType()}\n"
    }
    def load(String input) {
        // turn one line into multi-line "method calls"
        def cleaned = input.split(/\s+/)
        cleaned.each() { meth -> this.&"${meth}"() }
    }
}
def c = new Coffee()
c.load("venti nonfat whip latte")
c.order()

I'm not sure if there is a better way to do this or not. Ideally I would like to have the dslAttr add something to the Coffee metaClass instead of just adding stuff to the instances, but this seems to do the trick for now.

The Ruby and Groovy implementations become fairly similar at this point. It's a great way to reduce the amount of boilerplate code you would need to normally write to implement this kind of thing in less dynamic languages.