EdvardM
10/9/2012 - 8:10 PM

Generic traverse method for hash-ish structures

Generic traverse method for hash-ish structures

module Traversable

  module InstanceMethods
    def map_recursive(*args, &block)
      Traversable.map_recursive(self, *args, &block)
    end
  end

  class << self
    def included(other)
      other.send(:include, InstanceMethods)
    end

    # recursively map a hash-like structure with keys and
    # arbitrary nested values
    def map_recursive(thing, *args, &block)
      case thing
      when Hash
        thing.each_with_object({}) do |(k, v), h|
          h[k] = case v
                when Hash
                  map_recursive(v, *args, &block)
                when Array
                  v.map { |e| map_recursive(e, *args, &block) }
                else
                  block.call(v, *args)
                end
        end
      else
        block.call(thing, *args)
      end
    end

    def insinuate(target)
      target.send(:include, Traversable)
    end
  end
end

### specs to show usage

describe Traversable do
  it "should allow recursive traversal of nested hashes with any values" do
    some_struct = {
      'l1_int' => 1,
      'l1_str' => 'kissa',

      'left_node' => { 'l2_int' => 42, 'l2_str' => 'meow' },
      'right_node' => { 'r2_int' => 10,  'r2_str' => 'woof', 'l3_ary' => ['foo', 1] },

      :exclamation => 'quux'
    }

    capitalize_or_multiply = lambda { |e|
      e.respond_to?(:capitalize) ? e.capitalize : e * 2
    }

    Traversable.map_recursive(some_struct, &capitalize_or_multiply).should == {
      'l1_int' => 2,
      'l1_str' => 'Kissa',

      'left_node' => { 'l2_int' => 84, 'l2_str' => 'Meow' },
      'right_node' => { 'r2_int' => 20, 'r2_str' => 'Woof', 'l3_ary' => ['Foo', 2] },

      :exclamation => 'Quux'
    }
  end

  it "should mixin with a Hash" do
    Traversable.insinuate(Hash)

    {:key => {:subkey => [1, 2], :a_key => 3}}.
      map_recursive {|i| 2*i}.
      should == {:key => {:subkey => [2, 4], :a_key => 6}}
  end
end