multipart-mixed

Ruby Mini-Pattern: Using Blocks To Handle Special Cases

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 = <<end
  Length : 20
   Width : 10
end

sample_bar_text = <<end
         Status : OK
Widget Notifier : ON
 Channel/Device : 0,1
end

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