How to define helper methods in magic DSL code
Many Ruby gems expose their API as a DSL instead of a set of classes. A popular example of this is RSpec, which lets you write tests like this:
describe User do
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = User.new(:first_name => 'Henry', :last_name => 'Cook')
user.full_name.should == 'Henry Cook'
end
end
endUnder the hood RSpec converts the describe something do syntax to plain Ruby classes and objects. However, RSpec never tells you the names of those magic classes and in fact they might not even have names. This makes it hard to find the right place for helper methods you'd like to use in your DSL blocks.
For the sake of this example, let's assume you'd like to extract the User.new(...) invokation into a helper method new_user(...).
Don't do this (although it happens to work):
def new_user(first_name, last_name)
User.new(:first_name => first_name, :last_name => last_name)
end
describe User do
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = new_user('Henry', 'Cook')
user.full_name.should == 'Henry Cook'
end
end
endBy defining a method in the void you are extending Object, the mother of all Ruby classes. This means that every single object now has a new_user method.
A more humble way to define that method is to use a lambda:
describe User do
new_user = lambda do |first_name, last_name|
User.new(:first_name => first_name, :last_name => last_name)
end
describe '#full_name' do
it 'should return the concatenated first and last names' do
user = instance_exec('Henry', 'Cook', &new_user)
user.full_name.should == 'Henry Cook'
end
end
endHere new_user is a local variable that happens to contain a method. You can invoke it with instance_exec. It's not visible to other classes but can still be captured by every code block below it – exactly what we need.
We use the lambda-technique to DRY up our Rails routes machinist blueprints, and, of course, RSpec examples.
You can follow any response to this post through the Atom feed.


