Friday, February 15, 2008
Implementing an internal DSL in Ruby
On Christmas Day 2007 I released the 0.0.1 version of expectations. Expectations is the result of several months of designing what I wanted the Domain Specific Language (DSL) to look like, and a few days of making it executable. To design the DSL, I created what I was looking for without worrying about implementation. This entry is going to focus on what it took to take the DSL from looking good to actually executing.
The following code is an example of what I decided I wanted the DSL to look like (for a state based test, expectations also has support for behavior based testing).
When making a DSL executable I generally begin by building up objects with the values expressed in the DSL. For example, the above code creates a test suite containing 1 expectation where the expected value is :expected and the actual value can be determined by evaluating the block. While designing expectations I tested it using expectations, but that's probably more complex than is necessary for this example, so I'll simply print values instead.
The first step is ensuring that an Expectations::Suite is created when Expectations is called with a block. The following code demonstrates that an Expectations::Suite is created successfully. The code executes since the block given to Expectations is never executed.
note: Expectations::Suite is a singleton so that it can be appended to multiple times and later used for execution without being stored within another object.
The next step is to verify the number of expectations the suite stores for later execution. The following code verifies that one object is stored in the expectations array.
The final step is to create an object for each expectation and verify the values of both the expected and the actual.
note: expectations actually defers determining the value of actual until the suite is executed, but that's outside the scope of this example.
The bottom two lines of the last example show that the Expectations::Expectation instance is being populated successfully.
That's it, at this point the DSL can successfully be parsed. Of course, making the expectations run to verify that the values match still needs to be done, but that's no different than working with any plain old ruby code. The DSL can be evaluated and the framework can work with the objects that have been created.
As the example demonstrates, designing an internal DSL in Ruby is very easy largely due to open classes, class methods, and the ability to eval. The above code for evaluating the DSL is less than 30 lines of code.
The following code is an example of what I decided I wanted the DSL to look like (for a state based test, expectations also has support for behavior based testing).
Expectations do
expect :expected do
:expected
end
endWhen making a DSL executable I generally begin by building up objects with the values expressed in the DSL. For example, the above code creates a test suite containing 1 expectation where the expected value is :expected and the actual value can be determined by evaluating the block. While designing expectations I tested it using expectations, but that's probably more complex than is necessary for this example, so I'll simply print values instead.
The first step is ensuring that an Expectations::Suite is created when Expectations is called with a block. The following code demonstrates that an Expectations::Suite is created successfully. The code executes since the block given to Expectations is never executed.
; end
include Singleton
end
Expectations::Suite.instance
end
Expectations do
expect :expected do
:expected
end
end
Expectations::Suite.instance # => #<Expectations::Suite:0x1d4fc>
note: Expectations::Suite is a singleton so that it can be appended to multiple times and later used for execution without being stored within another object.
The next step is to verify the number of expectations the suite stores for later execution. The following code verifies that one object is stored in the expectations array.
; end
include Singleton
attr_accessor :expectations
self.expectations = []
end
expectations << :expectation
end
end
Expectations::Suite.instance.instance_eval(&block)
end
Expectations do
expect :expected do
:expected
end
end
Expectations::Suite.instance # => #<Expectations::Suite:0x1c82c @expectations=[:expectation]>
Expectations::Suite.instance.expectations.size # => 1
The final step is to create an object for each expectation and verify the values of both the expected and the actual.
; end
include Singleton
attr_accessor :expectations
self.expectations = []
end
expectations << Expectations::Expectation.new(expected, actual)
end
end
attr_accessor :expected, :actual
self.expected, self.actual = expected, actual.call
end
end
Expectations::Suite.instance.instance_eval(&block)
end
Expectations do
expect :expected do
:expected
end
end
Expectations::Suite.instance # => #<Expectations::Suite:0x1b184 @expectations=[#<Expectations::Expectation:0x1b0e4 @expected=:expected, @actual=:expected>]>
Expectations::Suite.instance.expectations.size # => 1
Expectations::Suite.instance.expectations.first.expected # => :expected
Expectations::Suite.instance.expectations.first.actual # => :expected
note: expectations actually defers determining the value of actual until the suite is executed, but that's outside the scope of this example.
The bottom two lines of the last example show that the Expectations::Expectation instance is being populated successfully.
That's it, at this point the DSL can successfully be parsed. Of course, making the expectations run to verify that the values match still needs to be done, but that's no different than working with any plain old ruby code. The DSL can be evaluated and the framework can work with the objects that have been created.
As the example demonstrates, designing an internal DSL in Ruby is very easy largely due to open classes, class methods, and the ability to eval. The above code for evaluating the DSL is less than 30 lines of code.
Labels: DSL, internal DSL, ruby


