In the previous three entries in this series of postings, I have been exploring the basics of creating domain specific languages using Ruby. Since I cannot disclose details of my big financial service client’s DSL, I am using this made up example to illustrate the techniques that are useful when building your own Ruby DSL. So far we have created a “PetShop” domain that allows us to interact with the Pets. Defining the behavior of new Pets is the purpose of this DSL.
For example, let’s say we now want each Pet to be able to do some trick, when called on to do so. When you are defining behaviors, it is very useful to define a recipe-like syntax in a DSL.
Here is an example:
pet "Toto" do
when\_performing\_tricks do
sit
speak
end
end
This is just a description or recipe of what the animal is supposed to do when asked to perform it’s trick. When we really want our Pet to perform, we simply say:
pet.perform
This produces the following output:
Toto will now perform...
Toto is sitting...
Toto says 'Woof!'
Let's hear some applause for Toto!
Slugworth will now perform...
Slugworth is sitting...
Let's hear some applause for Slugworth!
Tweety will now perform...
Tweety is sitting...
Tweety says 'I thought I saw a putty tat!
Tweety is flying...
Let's hear some applause for Tweety!
The Pet will do whatever tricks it knows, using this little simple bit of Ruby magic:
def self.when\_performing\_tricks(&routine)
@routine = routine
end
def perform
puts "#{name} will now perform..."
@routine.call
puts "Let's hear some applause for #{name}!"
end
The “when_performing_tricks” method simply stores the block, and executes it only when the time comes to “perform”.
So how does the Pet know what is entailed in each trick? I have put the “sit” trick into the base Pet class (every Pet knows how to sit):
def self.sit
puts "#{name} is sitting..."
end
We can also more importantly define custom tricks per type of Pet. A simple bit of Ruby metaprogramming goodness helps us out:
def self.define\_trick(name, &trick\_definition)
singleton\_class.class\_eval do
define\_method name, &trick\_definition
end
end
The “class_eval” methos lets us evaluate the inside expression in the context of the class object, not just a particular instance of the object. Another bit worth noting is the helper method that I have added to the DSLThing base class called “singleton_class” that looks like this:
def self.singleton\_class
class << self; self; end
end
This is just a short cut to get to the class instance object, known better to Rubyists as the “singleton_class”.
Lastly, the “define_method” method then lets us define a new method for the trick. When we want to define a new trick for a particular Pet, we can simply define it like this:
define\_trick "speak" do
puts "Toto says 'Woof!'"
end
Putting it all together, here are our new definitions of the tricks our Pets can do:
pet "Toto" do
when\_performing\_tricks do
sit
speak
end
define\_trick "speak" do
puts "Toto says 'Woof!'"
end
end
pet "Tweety" do
when\_performing\_tricks do
sit
speak
fly
end
define\_trick "speak" do
puts "Tweety says 'I thought I saw a putty tat!"
end
define\_trick "fly" do
puts "Tweety is flying..."
end
end
pet "Slugworth" do
when\_performing\_tricks do
sit
end
end
One other interesting technique of note is that we are actually defining a new type of Pet by dynamically declaring a new class based on the Pet class. Otherwise, our custom tricks for one type of Pet might interfer with the custom tricks of another. The solution to this is using the “Object.const_set” and “Object.const_get”. Here is the code I used:
def self.pet(name, &blk)
@pets ||= Hash.new
klass = Class.new(Pet)
Object.const\_set(name, klass) if not Object.const\_defined?(name)
p = Object.const\_get(name).new
p.name = name
p.class.class\_eval(&blk) if block\_given?
p.copyvars
@pets\[name\] = p
end
In this posting I have used the DSL recipe technique, and created dynamic methods and classes. Combining these techniques can allow for a very powerful and yet concise syntax when creating your own Ruby domain specific languages.
Here is the complete listing of the code from this post:
class DSLThing
def copyvars
self.class.instance\_variables.each do |var|
instance\_variable\_set(var, self.class.instance\_variable\_get(var))
end
end
def self.singleton\_class
class << self; self; end
end
end
class PetShop < DSLThing
attr\_accessor :pets, :people
def self.create(&block)
f = PetShop.new
f.class.instance\_eval(&block) if block\_given?
f.copyvars
return f
end
def self.pet(name, &blk)
@pets ||= Hash.new
klass = Class.new(Pet)
Object.const\_set(name, klass) if not Object.const\_defined?(name)
p = Object.const\_get(name).new
p.name = name
p.class.class\_eval(&blk) if block\_given?
p.copyvars
@pets\[name\] = p
end
end
class Animal < DSLThing
attr\_accessor :name
def initialize(name=nil)
@name = name
end
end
class Pet < Animal
def initialize(name=nil)
@name = name
super
end
def self.when\_performing\_tricks(&routine)
@routine = routine
end
def self.define\_trick(name, &trick\_definition)
singleton\_class.class\_eval do
define\_method name, &trick\_definition
end
end
def perform
puts "#{name} will now perform..."
@routine.call
puts "Let's hear some applause for #{name}!"
end
def self.sit
puts "#{name} is sitting..."
end
end
shop = PetShop.create do
pet "Toto" do
when\_performing\_tricks do
sit
speak
end
define\_trick "speak" do
puts "Toto says 'Woof!'"
end
end
pet "Tweety" do
when\_performing\_tricks do
sit
speak
fly
end
define\_trick "speak" do
puts "Tweety says 'I thought I saw a putty tat!"
end
define\_trick "fly" do
puts "Tweety is flying..."
end
end
pet "Slugworth" do
when\_performing\_tricks do
sit
end
end
end
shop.pets.each\_value do |pet|
pet.perform
end