Image of Aaron Gough, Software Developer

RSpec and Internal DSLs in Ruby

I had a developer email me the other day asking for advice on how to tackle a project that required parsing a natural language DSL (Domain Specific Language). He asked if maybe he should look at the parser for RSpec to get some ideas from there.

What some may not realize is that RSpec is a shining example of an ‘internal DSL’ ie: a DSL that is implemented using only the features of the parent language. Writing a DSL this way does not require that you create a separate parser and interpreter. This obviously saves a huge amount of development time in situations that allow using this style of DSL.

The down-side is that you can’t really expose a DSL written this way to un-trusted users. And by un-trusted users I really mean anyone that is not on the development team. The reason is that because an internal DSL makes use of the power of the parent language, it also generally allows users to execute arbitrary code, which is obviously a no-no.

Here’s a quick example of the style of code that RSpec uses to provide it’s natural syntax:

 1 module Kernel
 2   def should
 3     return Handlers::PositiveOperatorHandler.new(self)
 4   end
 5   
 6   def should_not
 7     return Handlers::NegativeOperatorHandler.new(self)
 8   end
 9 end
10 
11 module Handlers
12   class PositiveOperatorHandler
13     def initialize(actual)
14       @actual = actual 
15     end
16     
17     def ==(other)
18       puts "Expected '#{@actual.inspect}' to equal '#{other.inspect}'" unless @actual == other
19     end
20   end
21   
22   class NegativeOperatorHandler
23     def initialize(actual)
24       @actual = actual 
25     end
26     
27     def ==(other)
28       puts "Expected '#{@actual.inspect}' to not equal '#{other.inspect}'" if @actual == other
29     end
30   end
31 end
32 
33 # Example usage:
34 1.should == 3
35 1.should == 1
36 
37 1.should_not == 1
38 1.should_not == 3

RSpec is implemented very similarly to this, but is much more complex in order to provide all the features we love.

This method of creating a DSL really takes advantage of the fact that nearly everything in Ruby is a method call. Other languages that do not operate this way make it much harder to write a DSL in this manner, which is why you don’t see too many clones of RSpec in other languages.

For more info on writing internal DSLs I would suggest reading Ruby Best Practices or having a look through the RSpec code.