Ruby

Building a basic DSL to create callbacks in Ruby


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

Beginner
Administrate review
Beginner
Fixing Rspec and Cucumber random failures in your Continuous Integration Service
Best Practices
De Código, Café y Cervezas 07 – ¿Somos profesionales?