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
- Embedded DSL
- Ruby
- Builder by Jim Weirich