When implementing a generic processing method that needs to handle a special case (for example, in a parser), use blocks to elegantly handle these cases instead of writing the code inline.

Applicability

This mini-pattern is specific to Ruby, and while my example deals with parsing text, the technique could be useful in other situations.

Motivation

Recently I needed to create Ruby objects based on output from a command-line program. The output was generally well-structured, but not completely. As a result of these here-and-there inconsistencies, my parser turned into a real mess. Here’s a simplified version of the text to be parsed:

# Text describing a "Foo" object
Length : 20
 Width : 10

# Text describing a "Bar" object
         Status : OK
Widget Notifier : ON
 Channel-Device : 0,1

And here’s the classes I want to create from that text:

# Easy case
class Foo
  attr_accessor :length, :width
end

# Little bit harder
class Bar
  attr_accessor :status, :notifier, :channel, :device
end

Foo is the easiest possible case; you simply take each line and split around the “ : “. The left side matches the attribute name, the right side matches its value. But what about Bar? Two issues complicate things: the names don’t match right, and one line has two attributes. What to do now?

Implementation

The best way to handle mapping text names to attribute names is with a hash. Sometimes you can get away with applying transforms to the text–in the Foo example, just downcase the name–but in real life you usually have too many exceptions to the rule. The hash for Foo would look like this:

# Map Foo text keys => attributes
map = {
  "Length" => :length=,
  'Width'  => :width=
}

# (This code is in a separate parser class...)
# Parser can now assign key/val pair to
# attribute like so:
foo_instance.send(map[key], val)

The last line calls the attribute setter for the key (length, width), and passes along the value.

Now for Bar. Rather than map “Channel-Device” to a symbol, map it to a block instead:

# Map Bar text keys => attributes
map = {
  "Status"          => :status=,
  "Widget Notifier" => :notifier=,
  "Channel-Device"  => lambda do |bar, text|
    channel, device = text.split(/,/)
    bar.channel = channel
    bar.device = device
  end

# Parser now checks for mapping type
case (map[key])
  when Proc:   map[key].call(item, val) # <--- Call the proc
  when Symbol: item.send(map[key], val)
end

The lambda creates an object of class Proc, and when the parser sees a Proc for a key, it just calls the Proc. This lets the parser remain highly generic, and cleanly packages the special-case handling in our key/attribute map.

Credit

The idea for this technique was based on a discussion with Chad Fowler and Ara Howard at the Boulder/Denver Ruby User’s Group. While this isn’t rocket science – in fact it’s pretty common stuff for Ruby and other languages with closures – it’s a handy trick which is worth documenting.

Full Source Code

The full example file is presented below.

sample_foo_text = <<endtext
  Length : 20
   Width : 10
endtext

sample_bar_text = <<endtxt
         Status : OK
Widget Notifier : ON
 Channel-Device : 0,1
endtext

class Foo
  attr_accessor :length, :width

  def self.create_from_text(text)
    map = {
      "Length" => :length=,
      'Width'  => :width=
    }

    Parser.create_from_text(Foo, map, text)
  end
end

class Bar
  attr_accessor :status, :notifier, :channel, :device

  def self.create_from_text(text)
    map = {
      "Status"          => :status=,
      "Widget Notifier" => :notifier=,
      "Channel-Device"  => lambda do |bar, text|
        channel, device = text.split(/,/)
        bar.channel = channel
        bar.device = device
      end
    }

    Parser.create_from_text(Bar, map, text)
  end
end

# One parser class can create several Foo/Bar/etc. 
# classes given a class and text.
#
class Parser
  def self.create_from_text(item_class, map, text)
    item = item_class.new

    text.each_line do |line|
      key, val = line.split(/:/)
      key.strip!
      val.strip!

      case (map[key])
        when Proc:   map[key].call(item, val)
        when Symbol: item.send(map[key], val)
      end
    end

    item
  end
end

foo = Foo.create_from_text(sample_foo_text)
puts "Foo:"
puts foo.length
puts foo.width

bar = Bar.create_from_text(sample_bar_text)
puts "Bar:"
puts bar.status
puts bar.notifier
puts bar.channel
puts bar.device