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.