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
end

Under 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
end

By 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

end

Here 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.

Avatar

Sun, 15 Aug 2010 10:12:00 GMT

by henning

Tags: