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.