--3wXLZ5Iqn2laxPaVCRn
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

My solution started off from the most basic hash to openstruct
conversion I could think of: OpenStruct.new(some_hash). Those pesky
nested hashes still needed to be dealt with, so I came up with:

class Hash
  def to_ostruct(clz  penStruct)
    clz.new Hash[*inject([]){|ar,(k,v)|ar<<k<<(v.to_ostruct(clz) rescue v)}]
  end
end

This works, but it's very inefficient, it doesn't pass the case
Mentalguy posted, and it doesn't fail well with invalid keys or other
errors. To handle those things, I had to go a bit longer:

class Hash
  def to_ostruct(clz  penStruct, cch  })
    cch[self]  os  lz.new)
    each do |k,v|
      raise "Invalid key: #{k}" unless k /[a-z_][a-zA-Z0-9_]*/
      os.__send__("#{k} v.is_a?(Hash)? cch[v] || v.to_ostruct(clz,cch) : v)
    end
    os
  end
end

I chose to fail for invalid keys, rather than introducing potentially
confusing renaming rules or similar. It's still not as efficient as it
might be, but a bit better than the first one.

Neither solution takes into consideration the problems Ara pointed out -
this is the reason for the optional 'clz' parameter to both methods.
Undef'ing methods from OpenStruct turned out to be a non-starter, since
it uses them itself, so I just implemented a simple, naive DumbStruct
that can be used with the to_ostruct methods above:

class DumbStruct
  alias :__iv_set__ :instance_variable_set
  alias :__class__ :class
  instance_methods.each do |m|
    undef_method(m) unless m /^(__|method_missing|inspect|to_s)|\?$/
  end

  def initialize(hsh  })
    hsh.each { |k,v| method_missing("#{k} v) }
  end

  def method_missing(name, *args, &blk)
    if (name  ame.to_s) /[^$/
      name  ame[0..-2]
      __iv_set__("@#{name}", args.first)
      (class << self; self; end).class_eval { attr_accessor name }
    else
      super
    end
  end
end

Attached are the full files including testcases and a basic benchmark.
Thanks for another fun and interesting quiz :)

-- 
Ross Bamford - rosco / roscopeco.REMOVE.co.uk

--3wXLZ5Iqn2laxPaVCRn
Content-Disposition: attachment; filename=dstruct.rb
Content-Type: text/plain; name=dstruct.rb; charset=utf-8
Content-Transfer-Encoding: 7bit

# An mostly-compatible OpenStruct replacement that is, oddly, faster than 
# OpenStruct some of the time, and crucially allows us to use Object method
# names for struct members. Needed to pass Ara's test case. To use, just
# pass it to to_ostruct:
#
#   ds  :class 1, :method 2}.to_ostruct(DumbStruct)
#   
class DumbStruct  
  alias :__iv_set__ :instance_variable_set
  alias :__class__ :class
  instance_methods.each do |m| 
    undef_method(m) unless m /^(__|method_missing|inspect|to_s)|\?$/
  end

  def initialize(hsh  })
    hsh.each { |k,v| method_missing("#{k} v) }
  end
  
  def method_missing(name, *args, &blk)
    if (name  ame.to_s) /[^$/
      name  ame[0..-2]
      __iv_set__("@#{name}", args.first)
      (class << self; self; end).class_eval { attr_accessor name }
    else
      super
    end
  end
end


--3wXLZ5Iqn2laxPaVCRn
Content-Disposition: attachment; filename=hash2ostruct-basic.rb
Content-Type: text/plain; name=hash2ostruct-basic.rb; charset=utf-8
Content-Transfer-Encoding: 7bit

require 'ostruct'

class Hash
  # Convert this hash to a tree of nested OpenStructs (or equivalent class
  # if specified - see DumbStruct). This is the first version I wrote, and
  # it doesn't take account of the evil stuff Mentalguy demo'd, or of illegal
  # method name keys as Adam Shelly pointed out. It's a simple recursive
  # solution, and very inefficient.
  def to_ostruct(clz  penStruct)
    clz.new Hash[*inject([]){|ar,(k,v)|ar<< k<<(v.to_ostruct(clz) rescue v)}]
  end
end

if $0 __FILE__
  require 'yaml'
  require 'test/unit'
  require 'benchmark'
  require 'dstruct'
  
  class TestHashToOstruct < Test::Unit::TestCase
    TD1  AML::load(%{
      ---
      foo: 1
      bar:
        baz: [1, 2, 3]
        quux: 42
        doctors:
          - William Hartnell
          - Patrick Troughton
          - Jon Pertwee
          - Tom Baker
          - Peter Davison
          - Colin Baker
          - Sylvester McCoy
          - Paul McGann
          - Christopher Eccleston
          - David Tennant
        a: {x: 1, y: 2, z: 3}
    })

    TD2  AML::load(%{
      ---
      foo: 1
      bar:
        baz: [1, 2, 3]
        quux: 42
        doctors:
          - William Hartnell
          - Patrick Troughton
          - Jon Pertwee
          - Tom Baker
          - Peter Davison
          - Colin Baker
          - Sylvester McCoy
          - Paul McGann
          - Christopher Eccleston
          - David Tennant
        a: {x: 1, y: 2, z: 3}
      table: walnut
      method: linseed oil
      type: contemporary
      id: 1234
      send: fedex
    })

    def test_01
      os  D1.to_ostruct
     
      assert_equal 1, os.foo
      
      assert_equal [1,2,3], os.bar.baz
      assert_equal ['William Hartnell', 'Patrick Troughton', 'Jon Pertwee',
                    'Tom Baker', 'Peter Davison', 'Colin Baker', 
                    'Sylvester McCoy', 'Paul McGann', 'Christopher Eccleston',
                    'David Tennant'], os.bar.doctors

      assert_equal 1, os.bar.a.x
      assert_equal 2, os.bar.a.y
      assert_equal 3, os.bar.a.z
    end

    def test_02 # ara - need to use DumbStruct to pass this one
      os  D2.to_ostruct(DumbStruct)
     
      assert_equal 1, os.foo
      
      assert_equal [1,2,3], os.bar.baz
      assert_equal ['William Hartnell', 'Patrick Troughton', 'Jon Pertwee',
                    'Tom Baker', 'Peter Davison', 'Colin Baker', 
                    'Sylvester McCoy', 'Paul McGann', 'Christopher Eccleston',
                    'David Tennant'], os.bar.doctors
      
      assert_equal 1, os.bar.a.x
      assert_equal 2, os.bar.a.y
      assert_equal 3, os.bar.a.z
      
      assert_equal 'walnut', os.table
      assert_equal 'linseed oil', os.method
      assert_equal 'contemporary', os.type
      assert_equal 1234, os.id
      assert_equal 'fedex', os.send
    end
  end

  if ARGV.delete('--bm')
    puts "#### Basic impl - OpenStruct ####"
    Benchmark.bm do |x|
      x.report('base  ') { 5000.times { TestHashToOstruct::TD1.to_ostruct } }
      x.report('ara   ') { 5000.times { TestHashToOstruct::TD2.to_ostruct } }
    end

    puts "\n#### Basic impl - DumbStruct ####"
    d  umbStruct
    Benchmark.bm do |x|
      x.report('base  ') { 5000.times {TestHashToOstruct::TD1.to_ostruct(d)}}
      x.report('ara   ') { 5000.times {TestHashToOstruct::TD2.to_ostruct(d)}}
    end

    puts "\n####   tests   ####"
  end
end


--3wXLZ5Iqn2laxPaVCRn
Content-Disposition: attachment; filename=hash2ostruct.rb
Content-Type: text/plain; name=hash2ostruct.rb; charset=utf-8
Content-Transfer-Encoding: 7bit

require 'ostruct'

class Hash
  # Convert this hash to a tree of nested OpenStructs (or equivalent class
  # if specified - see DumbStruct). This version passes all the test-cases
  # supplied on the ML, though it does need to be used with DumbStruct in 
  # order to pass the test Ara posted, and I chose to fail fast in the 
  # case of illegal method names in the hash keys. It's another recursive 
  # solution, and though it's a bit quicker than the first one, it's still
  # no speed demon.
  def to_ostruct(clz  penStruct, cch  })
    cch[self]  os  lz.new)
    each do |k,v| 
      raise "Invalid key: #{k}" unless k /[a-z_][a-zA-Z0-9_]*/      
      os.__send__("#{k} v.is_a?(Hash)? cch[v] || v.to_ostruct(clz,cch) : v)
    end
    os
  end
end

if $0 __FILE__
  require 'yaml'
  require 'test/unit'
  require 'benchmark'
  require 'dstruct'
  
  class TestHashToOstruct < Test::Unit::TestCase
    TD1  AML::load(%{
      ---
      foo: 1
      bar:
        baz: [1, 2, 3]
        quux: 42
        doctors:
          - William Hartnell
          - Patrick Troughton
          - Jon Pertwee
          - Tom Baker
          - Peter Davison
          - Colin Baker
          - Sylvester McCoy
          - Paul McGann
          - Christopher Eccleston
          - David Tennant
        a: {x: 1, y: 2, z: 3}
    })

    TD2  AML::load(%{
      ---
      foo: 1
      bar:
        baz: [1, 2, 3]
        quux: 42
        doctors:
          - William Hartnell
          - Patrick Troughton
          - Jon Pertwee
          - Tom Baker
          - Peter Davison
          - Colin Baker
          - Sylvester McCoy
          - Paul McGann
          - Christopher Eccleston
          - David Tennant
        a: {x: 1, y: 2, z: 3}
      table: walnut
      method: linseed oil
      type: contemporary
      id: 1234
      send: fedex
    })

    TD3  AML::load(%{
      ---
      &verily
      lemurs:
        unite: *verily
        beneath:
          - patagonian
          - bread
          - products
      thusly: [1, 2, 3, 4]
    })
     
    TD4  AML::load(%{
      ---
      1: for the money
      2: for the show
      3: to get ready
      4: go go go
    })
                     
    def test_01
      os  D1.to_ostruct
     
      assert_equal 1, os.foo
      
      assert_equal [1,2,3], os.bar.baz
      assert_equal ['William Hartnell', 'Patrick Troughton', 'Jon Pertwee',
                    'Tom Baker', 'Peter Davison', 'Colin Baker', 
                    'Sylvester McCoy', 'Paul McGann', 'Christopher Eccleston',
                    'David Tennant'], os.bar.doctors

      assert_equal 1, os.bar.a.x
      assert_equal 2, os.bar.a.y
      assert_equal 3, os.bar.a.z
    end

    def test_02 # ara - need to use DumbStruct to pass this one
      os  D2.to_ostruct(DumbStruct)
     
      assert_equal 1, os.foo
      
      assert_equal [1,2,3], os.bar.baz
      assert_equal ['William Hartnell', 'Patrick Troughton', 'Jon Pertwee',
                    'Tom Baker', 'Peter Davison', 'Colin Baker', 
                    'Sylvester McCoy', 'Paul McGann', 'Christopher Eccleston',
                    'David Tennant'], os.bar.doctors
      
      assert_equal 1, os.bar.a.x
      assert_equal 2, os.bar.a.y
      assert_equal 3, os.bar.a.z
      
      assert_equal 'walnut', os.table
      assert_equal 'linseed oil', os.method
      assert_equal 'contemporary', os.type
      assert_equal 1234, os.id
      assert_equal 'fedex', os.send
    end

    def test_03 # mental
      os  D3.to_ostruct

      assert_equal OpenStruct, os.lemurs.unite.lemurs.unite.lemurs.class
      assert_same os.lemurs, os.lemurs.unite.lemurs
      assert_equal ['patagonian', 'bread', 'products'], os.lemurs.beneath
      assert_same os.lemurs.beneath, os.lemurs.unite.lemurs.beneath
      assert_equal [1,2,3,4], os.thusly
      assert_same os.thusly, os.lemurs.unite.thusly
    end

    def test_04 # adam shelly
      assert_raise(RuntimeError) do
        os  D4.to_ostruct
      end
    end
  end
  
  if ARGV.delete('--bm')
    puts "#### Better impl - OpenStruct ####"
    Benchmark.bm do |x|
      x.report('base  ') { 5000.times { TestHashToOstruct::TD1.to_ostruct } }
      x.report('ara   ') { 5000.times { TestHashToOstruct::TD2.to_ostruct } }
      x.report('mental') { 5000.times { TestHashToOstruct::TD3.to_ostruct } }
    end

    puts "\n#### Better impl - DumbStruct ####"
    d  umbStruct
    Benchmark.bm do |x|
      x.report('base  ') { 5000.times {TestHashToOstruct::TD1.to_ostruct(d)}}
      x.report('ara   ') { 5000.times {TestHashToOstruct::TD2.to_ostruct(d)}}
      x.report('mental') { 5000.times {TestHashToOstruct::TD3.to_ostruct(d)}}
    end

    puts "\n####   tests   ####"
  end
end


--3wXLZ5Iqn2laxPaVCRn--