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 want 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