Metaprogramming Ruby, Pt. 1: Markaby

By Loren Segal on July 07, 2009 at 718:640:356 PM

With the recent beta release (books can be beta now?) of Metaprogramming Ruby by Paolo Perrotta, I figured I’d share some of my personal knowledge and tricks in the field of meta-programming. Hopefully I can make this a multi-part series of articles on the subject.

In this first article I’m going to implement a standard Markaby-style DSL that takes declarative Ruby method calls and translates them into HTML. The example only really highlights one metaprogramming trick, namely passing a block to instance_eval to have code executed in the context of an arbitrary object. The rest is just fluff, so it should be simple enough to follow.

Markamini, Our Example DSL

So we want to make a simple DSL where whichever declarative method we call inside a block will be turned into an HTML-like tag in our output, much the way Markaby works. We’ll call this “Markamini”. For example, we should be able to write the following:

Note: I use Ruby 1.9 syntax in all of my examples.

html = Markamini.document do
  html do
    head do
      meta name: 'charset', content: 'utf-8'
      title 'hi'
    end
    body do
      div(id: 'foo') { "A div block" }
    end
  end
end
puts html

And get the HTML:

<html>
  <head>
    <meta name="charset" content="utf-8" />
    <title>hi</title>
  </head>
  <body>
    <div id="foo">A div block</div>
  </body>
</html>

The Implementation

You can probably guess that method_missing (the catch-all method call in Ruby) will be involved here in order to handle our arbitrary method calls. The question is, where should this method_missing be defined? From the example above, it looks like we’re actually executing the methods html, head, etc. in the same context as Markamini.document. Do you think we’d define method_missing globally? That would be ugly, wouldn’t it. You’ll be glad to know we’re not going to do that. Instead, we use instance_eval and blocks to trick Ruby into running this code in a localized context. This is where the magic happens.

Blocks are Procs, Procs are Objects, and Objects get passed around.

Remember that blocks are basically Proc’s, which means we don’t need to immediately execute their contents and can store these blocks for later or pass them around as the Proc’s they are. In this case we don’t have to store them for long, but we are going to avoid yielding to these blocks and instead use them as Proc objects. In fact, we’re going to pass the blocks (now a Proc) to another object for it to execute through instance_eval.

The Trick: instance_eval(&block)

That’s the first (and only) trick here. The block passed to Markamini.document is not yielded, but rather passed to Markamini itself to be executed in the context of a localized object. How do we do this? It’s simple:

def self.document(&block)
  new.instance_eval(&block).to_s
end

We pass the block Proc object as a block (we use the & prefix to tell Ruby that the argument is not an argument but an actual block) to instance_eval to have it run our code as if it was running inside an instance of Markamini. We could run this on an instance of any object, but since we’re already in that Markamini class we might as well use it. Now that our code is being eval’d as if it was run inside Markamini, our methods are being sent to the instance, so let’s define our method_missing:

def method_missing(name, opts = {}, &block)
  parent = @node
  @node = Node.new(parent, name, opts)
  parent.children << @node if parent
  if block_given?
    val = instance_eval(&block) 
    @node.children << val if val.is_a?(String)
  end
  parent ? @node = parent : @node
end

Most of this is fluff to create our doubly-linked list of Node objects to represent our tree of HTML elements. The one important line is the instance_eval(&block) once again in the middle. We take all inner blocks and send them all back to our Markamini instance, so methods are always being sent to the same method_missing.

The Implementation Fluff

So that’s the basics of implementing a Markaby-like DSL, but for completeness I’ll list the full source including the Node class:

class Markamini
  attr_accessor :node

  def self.document(&block)
    new.instance_eval(&block).to_s
  end

  def method_missing(name, opts = {}, &block)
    parent = @node
    @node = Node.new(parent, name, opts)
    parent.children << @node if parent
    if block_given?
      val = instance_eval(&block) 
      @node.children << val if val.is_a?(String)
    end
    parent ? @node = parent : @node
  end
end

class Node
  attr_accessor :name, :options, :children, :parent

  def initialize(parent, name, options = {})
    @options = options
    @name = name
    @parent = parent
    @children = []

    if options.is_a?(String)
      @options = nil
      @children = [options]
    end
  end

  def to_s
   if children.size > 0
      "<#{name}#{' ' + attrs unless attrs.empty?}>#{children.join}</#{name}>"
    else
      "<#{name}#{' ' + attrs unless attrs.empty?} />"
    end
  end

  def attrs
    return "" if options.nil?
    options.map {|k, v| "#{k}=#{v.inspect}" }.join(" ") 
  end
end

As mentioned above, our Node class implements a standard HTML element in a doubly-linked list (with a parent and child nodes). We then collapse the root node with #to_s which is called on all children nodes. We also setup attributes or check to see if we’re passed a String instead of attributes, or maybe our block yields a String value instead of elements. Standard HTML-DSL stuff.

What We’ve Learned

The important part is that by now you hopefully understand how to use instance_eval and execute a block of code in a certain context. This is the underlying fundamental concept behind every DSL out there, and should get you started writing your own Ruby DSLs.

Questions? Comments? Follow me on Twitter (@lsegal) or email me.