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
Comments:
<< Home
Jay, you seem to have missed a small detail in the second code example, in the implementation of the Kernel Expectation method. In the code it takes a &block but doesn't send that block along to anything. By design?
Ola, thanks for the comment.
I purposefully did nothing with the block so I could iteratively show building up the different objects. Perhaps it's confusing, but I like that you can paste the code in and run it to see that it works.
Anyway, the next example shows what I do with it. =)
Cheers, Jay
I purposefully did nothing with the block so I could iteratively show building up the different objects. Perhaps it's confusing, but I like that you can paste the code in and run it to see that it works.
Anyway, the next example shows what I do with it. =)
Cheers, Jay
It's not clear to me why you went to all this trouble. This "language" looks like on obfuscated way of making a few method calls. Wouldn't it be simpler to build and less brittle for end-users to just provide some basic OO methods to accomplish the same thing?
to mr anonymous:
i think you missed the point. this post was all about showing users how jay might go about writing DSLs which in turn might help other people who want to write DSLs.
In addition, Expectations is his take on doing testing.
i think you missed the point. this post was all about showing users how jay might go about writing DSLs which in turn might help other people who want to write DSLs.
In addition, Expectations is his take on doing testing.
Anonymous:
I'm not really sure how to respond.
> This "language" looks like on obfuscated way of making a few method calls.
Fair enough, what would you like to see instead?
> ...provide some basic OO methods to accomplish the same thing?
Again, I'm not sure what you are looking for. Can you provide an example?
Cheers, Jay
Post a Comment
I'm not really sure how to respond.
> This "language" looks like on obfuscated way of making a few method calls.
Fair enough, what would you like to see instead?
> ...provide some basic OO methods to accomplish the same thing?
Again, I'm not sure what you are looking for. Can you provide an example?
Cheers, Jay
<< Home




