Tuesday, May 13, 2008

Micro DSLs

Three things you need to know about me: I spend a lot of time on planes, I like DSLs and I'm lazy. With these powers combined, I've created my new hobby: creating micro DSLs. What is a micro DSL? Basically, during my (seemingly) never ending flights from San Fran to Chicago I try to recreate DSLs I like in as few lines as possible. Oh, and one more thing: I give the resulting DSLs self-degrading names to show the respect to the people who first created them.

A couple more rules: I decide how much of the original DSL I want to recreate and I can cut any corners that I'd like. In some cases, I try to improve the original DSL or tweak the syntax a bit.

The first DSL I've micro-ized is Jay Fields expectations. I find its syntax very nice and it had some unique challenges. The general approach I took to keep the code short was to use test/unit behind the scenes. This stopped me from having to recreate all the goodness that test/unit gives me for free: a test suite runner, error/failure tracking and assertions.

Anyway, the code is below. It's about 48 lines long and implements most of the features from the original DSL.
require 'test/unit'
require 'rubygems'
require 'mocha'

class Mocha::Expectation
def and
@mock
end

alias_method :go, :and
end

class LoweredExpectations
include Mocha::Standalone

COMPARATORS = {
Class => :kind_of?,
Range => :include?,
Regexp => :=~
}

def initialize
@test_case = Class.new(Test::Unit::TestCase)
@test_num = 0
end

def expect(value=true,&block)
test_name = "test_#{@test_num += 1}"

if value.kind_of? Mocha::Mock
@test_case.send(:define_method,test_name) do
block.call(value)
value.verify
end
else
comparator = COMPARATORS[value.class] || :==

@test_case.send(:define_method,test_name) do
assert(block.call.send(comparator,value))
end
end
end
end

def LoweredExpectations(&block)
LoweredExpectations.new.instance_eval(&block)
end

Below is an expectations file using the above code. Note there's a small different in the way mocks are used. The method and links expectations and go is used to send the mock to the block.

LoweredExpectations do
expect do
1 == 1
end

expect 2 do
1 + 1
end

expect "foo" do
"fo" + "o"
end

expect /bar\d/ do
"bar2"
end

expect Fixnum do
"4".to_i
end

expect mock.expects(:bar).and.expects(:baz).go do |foo|
foo.bar
foo.baz
end
end

Of course, the error reporting is much worse in my version than Jay's, but I'd say not too bad for ~50 lines of code. Let me know what you think.

Update:

Source is available on github. You can see my other projects on the right side of this blog.

0 comments: