This blog has been archived. Our writing has moved to makandra cards.
The blog of , a Ruby on Rails development team

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.

Growing Rails Applications in Practice
Check out our e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Recent posts

Our address:
makandra GmbH
Werner-von-Siemens-Str. 6
86159 Augsburg
Germany
Contact us:
+49 821 58866 180
info@makandra.de
Commercial register court:
Augsburg Municipal Court
Register number:
HRB 24202
Sales tax identification number:
DE243555898
Chief executive officers:
Henning Koch
Thomas Eisenbarth