Domain Specific Language Demo in Ruby

Fun with Ruby and Domain Specific Languages

Domain Specific Languages (DSLs) are great, because they enable you to express your ideas in a language which draws it’s vocabulary and structure from the business domain you are concerned with.

DSLs makes it easy to communicate with stakeholders and others with knowledge of the business domain. Also it’s much more natural and easier to solve problems in such a DSL for programmers if available, compared to a general purpose low-level programming language.

DSLs are often split up to external end internal DSLs. In this post I’m trying to show some meta-programming capabilities of Ruby. Here I build a simple internal DSL, on top of Ruby, using Ruby as the host language.

Please note that some sources may refer to internal DSLs as embedded DSLs. Don’t be confused, they both mean the same.

The domain in this post is the building of XML markup from structured DSL code.

Imagine you need to generate XML markup like this:

<person type="Villain" role="President">
  <name>
    <first>Coriolanus</first>
    <last>Snow</last>
  </name>
  <address>
    <street>Presidential Residence</street>
    <city>The Capitol</city>
    <country>Panem</country>
  </address>
  <empty/>
  <withattribsonly key="value" other="other"/>
</person>

Using code-editors and IDEs, it’s not a big deal today, since they aid entering structured data like XML and they can offer tag-closing and autocomplete features and more. Hence you can generate them by entering the XML by hand.

Also, you can opt for writing a small program in a general-purpose programming language to transform some structured data into XML markup, but that assumes that you already have the structured data available to you in some other format. By using a low-level general-purpose programming language you’ll have some performance gain, but coding the tool in that language will be tedious and cumbersome.

A third option could be to use an existing tool/library to perform the transformation for you. With this you could save the time and effort implementing such a tool by yourself.

BUT there’s a forth option, which is the most FUN option in my opinion. Since Ruby is a dynamic, high-level programming language with amazing meta-programming features, it’s so easy to write an internal DSL in Ruby, just to make the XML markup generating happen.

Take a look at the following little DSL for describing structured data:

to_xml do
  person type: 'Villain', role: 'President' do
    name do
      first 'Coriolanus'
      last 'Snow'
    end
    address do
      street 'Presidential Residence'
      city 'The Capitol'
      country 'Panem'
    end
    empty
    withattribsonly key: 'value', other: 'other'
  end
end

If you were looking careful, you probably discovered, that it’s the same structured data as the above in XML markup.

That’s it!

You can find all the source for this little demo below.

# - No explicit support for xml processing instructions
# - No explicit support for comments
# - Lacks support for escaping xml entities
# - No support for a predefined offset to start the indentation form
# - Fixed, non-configurable 2 space indentation
# ...
class SimpleBuilder < BasicObject
  def method_missing(name, *args, &block)
    @level ||= 0
    @target ||= ::STDOUT
    attrs, text = _split_args_to_attrs_and_text args
    if block
      ::Kernel.fail ::ArgumentError, 'Cannot have text content when sub-structure is given' unless text.nil?
      _indent
      _start_tag name, attrs
      _newline
      begin
        @level += 2
        block.call(self)
      ensure
        @level -= 2
        _indent
        _end_tag name
        _newline
      end
    elsif text.nil?
      _indent
      _start_tag name, attrs, true
      _newline
    else
      _indent
      _start_tag name, attrs
      @target << text
      _end_tag name
      _newline
    end
  end

  private

  def _start_tag(name, attrs, close = false)
    @target << "<#{name}"
    attrs.each do |k, v|
      @target << %( #{k}="#{v}")
    end if attrs
    @target << '/' if close
    @target << '>'
  end

  def _end_tag(name)
    @target << "</#{name}>"
  end

  def _newline
    @target << "\n"
  end

  def _indent
    return if @level == 0
    @target << ' ' * @level
  end

  def _split_args_to_attrs_and_text(args)
    attrs, text = nil, nil
    args.each do |arg|
      case arg
      when ::Hash
        attrs ||= {}
        attrs.merge!(arg)
      else
        text ||= ''
        text << arg.to_s
      end
    end
    [attrs, text]
  end
end

def to_xml(&block)
  sb = SimpleBuilder.new
  sb.instance_exec(&block)
end

Please note that this is a simplified solution lacking some important features which could be essential in a real production system -some of them were mentioned in the implementation as comments above-, but these features could be added later and the goal of this demo was only to show how easy it is to write an internal DSL in Ruby.

This code above was based on Builder which is a Ruby Gem available for all of us and is written by Jim Weirich. You may wish to see that too for a more elaborate solution.

References

  1. Embedded DSL
  2. Ruby
  3. Builder by Jim Weirich
Domain Specific Language Demo in Ruby