Do you know what a Domain Specific Language(DSL) is and how to implement one in Ruby?. This article aims to provide a slight introduction to this topic. It is divided in 3 sections, first we'll define what a DSL is, second we'll see some examples of DSL implementations, and third we'll build a DSL.
What is a DSL?
According wikipedia a DSL is defined as:
In software development and domain engineering, a domain-specific language (DSL) is a programming language or specification language dedicated to a particular problem domain, a particular problem representation technique, and/or a particular solution technique.
To clarify, we'll see some examples. As you read through them take into account the following points:
- Ruby blocks are used everywhere. They are the bare minimum construction element.
- Though the used structures are not part of the Ruby core, all of them use valid Ruby constructs.
- The main purpose of creating new code structure is to provide a more human readable code.
This implies that the following DSL examples(codes) are build with sentences like:
def describe(subject, &block); end
def Given(expression, &block); end
def get(route, &block); end
Be aware, its respective gems may not define each DSL as I did, but it helps to show different possible ways to do it.
Let's start with the examples.
DSL implementations
If you are doing Ruby then you probably have already used DSL's. Gems like RSpec, Cucumber and Sinatra are good examples of DSL implementations. Let's see their syntax and put special attention to the structures they use.
First, let's see three snippets from these languages.
Rspec snippet
In RSpec when you want to test if some object responds to a method call you usually write something like:
describe MyObject do
it 'should respond to a method call' do
subject.should respond_to(:method_call)
end
end
Cucumber snippet
In Cucumber when you write step definitions you do things like:
Given /^I click link "([^""]*)"$/ do |link|
find(:css, link).click
end
Sinatra snippet
In Sinatra when you whant to write a route/controller you do something like:
get "/" do
"Hi there"
end
Now let's write our own DSL.
Writing a DSL
The following technique intention is similar to that from Rails Controller Filters.
Let's build a DSL called Wrappable. Wrappable will be a simple custom DSL that wrap's' a method with callbacks using the Ruby language.
Let me clarify what I mean by wrap using the following snippet:
def before; end
def original; end
def after; end
Wrappable will wrap the original method. Whenever original is invoked, the before method will be automatically invoked first, second it will invoke the original method and finally it will invoke the after method.
The way the before and after methods behave is known as a Callback.
This behavior can also be achieved with something like:
def before; end
def original
before
# original sentences
after
end
def after; end
But, I want to do this dynamically, using a wrap method that will be able to setup the calls to the methods before and after programatically.
Usage example
We will end up the example with the following CallbackTest class:
class CallbackTest
# This module contains the whole functionality
include Wrappable
def original
puts "Original method"
end
def before
puts "Before method"
end
def after
puts "After method"
end
wrap :original do
before_run :before
after_run :after
end
end
CallbackTest.new.original
The script output should be as follows:
...$ ruby my_test.rb
Before method
Original method
After method
Writing the callback step by step
In order to build the whole example let's start by listing what we require to do and then we will code the example from scratch.
The requirements
Consider the script fragment where wrap is invoked:
wrap :original do
before_run :before
after_run :after
end
This means:
- Step 1, we need a class method called wrap. The wrap method has two parameters: The first is the *symbol representing the name of the method that will be wrapped and the second is a block.
- Step 2, the block parameter contains two method calls that configure what methods should be invoked: before_run and after_run. Each method receives one parameter as symbol that represents the name of the method that will be invoked respectively.
- Step 3, create the wrap behavior. This step involves creating a new method that will eventually call the original, before and after methods and implies that we need to keep a reference to the original method so we do not overwrite it.
Step 1
What we are going to do here is: * Create a Wrappable module with an empty wrap method and its two parameters. * Add the Wrappable module methods to the CallbackTest metaclass (adding static methods). * Invoking the Wrappable's wrap method inside CallbackTest class.
Now, save the following snippet as callback_test.rb:
module Wrappable
def wrap(original_method, &block)
end
end
class CallbackTest
extend Wrappable
def original
puts "Original method"
end
wrap :original do
end
end
CallbackTest.new.original
Now execute it:
...$ ruby callback_test.rb
Original method
Step 2
What we are going to do here is:
- Invoke the before_run and after_run inside the wrap's parameter block.
- Create a WrapperOptions class.
- This class will namespace the before_run and after_run methods.
- I want the wrap's parameter block to be evaluated inside this WrapperOptions class.
- By using this class we can handle all options parsing in just one place.
Update callback_test.rb with the following:
module Wrappable
class WrapperOptions
def initialize(&block)
instance_eval(&block)
end
private
def before_run(method_name)
@before = method_name
end
def after_run(method_name)
@after = method_name
end
end
def wrap(original_method, &block)
wrapper_options = WrapperOptions.new(&block)
end
end
class CallbackTest
extend Wrappable
def original
puts "Original method"
end
def before
puts "Before method"
end
def after
puts "After method"
end
wrap :original do
before_run :before
after_run :after
end
end
CallbackTest.new.original
And verify that your script still works:
...$ ruby callback_test.rb
Original method
Step 3
Our CallbackTest remains unchanged. Let's improve and complete the Wrappable module. What we are going to do here is:
- Alias the original method so we don't overwrite it.
- Create accessors to our wrapped method names in the WrapperOptions class.
Create a new method that will eventually call the original, before and after methods.
module Wrappable class WrapperOptions attr_reader :before, :after def initialize(&block) instance_eval(&block) end private def before_run(method_name) @before = method_name end def after_run(method_name) @after = method_name end end def wrap(original_method, &block) wrapper_options = WrapperOptions.new(&block) alias_method :old_method, original_method define_method original_method do send(wrapper_options.before) send(:old_method) send(wrapper_options.after) end end end
And finally test that the whole this script works:
...$ ruby my_test.rb
Before method
Original method
After method
That's it.
Conclusion
This was a simple way to build a custom DSL, there are many DSL examples all around the web for example:
Note that this implementation only supports method names as parameters, if you want to view an example of transparently supporting blocks as wrappers then look at the complete exercise gist.
Finally, if you really need to implement callbacks then I recommend you to look at the ActiveSupport::Callbacks package.
Thank you for reading.
Regards
