Yes, no, maybe?

--- Original Message ---

On Sunday 14 November 2004 06:16 pm, Yukihiro Matsumoto wrote:
| Can you be more specific, preferably with code example?
|
|        matz.

Ah, what the hek! I pulled down 1.9 and modified. Below you will find code. 
Let me know if you'd like me to send code as attachment via private mail.

Okay so the trick was simply to move everything into a mixin module (expect 
the initialize method). I also changed @table to @__table__ to help prevent 
possible name clash. The only other changed required was using 

  @__table__ ||= {} 

in a number of places (too bad no better way to do that). Finally I just 
called the module OpenStructable --seems like a good a name as any.

Also, I found what looks to be a bug in current 1.9 version. The initialize 
method wasn't calling the newly added #new_ostruct_member method. I fixed and 
added #update method. Also, is #new_ostruct_member supposed to be private? 
Currently it is public. If it remains public, I recommend a more generic 
name. Not sure what though.

Hope you like,
T.

------------

#
# = ostruct.rb: OpenStruct implementation
#
# Author:: Yukihiro Matsumoto 
# Contributor:: Thomas Sawyer
# Documentation:: Gavin Sinclair
#
# OpenStruct allows the creation of data objects with arbitrary attributes.
# See OpenStruct for an example. OpenStruct is implemented with a resuable
# mixin module OpenStructable.
#

# OpensStructable is a mixin module which can provide OpenStruct behavior to 
# any class or object.
#
#   require 'ostruct'
#
#   record = Object.new
#   record.extend OpenStructable
#
#   record.name    = "John Smith"
#   record.age     = 70
#   record.pension = 300
#   
#   puts record.name     # -> "John Smith"
#   puts record.address  # -> nil
#
module OpenStructable

  # Duplicate an OpenStruct object members. 
  def initialize_copy(orig)
    super
    @__table__ = @__table__.dup
  end

  def new_ostruct_member(name)
    self.instance_eval %{
      def #{name}; @__table__[:#{name}]; end
      def #{name}=(x); @__table__[:#{name}] = x; end
    }
  end

  #
  # Generate additional attributes and values.
  #
  def update(hash)
    @__table__ ||= {}
    if hash
      for k,v in hash
        @__table__[k.to_sym] = v
        new_ostruct_member(k)
      end
    end
  end
  
  def method_missing(mid, *args) # :nodoc:
    mname = mid.id2name
    len = args.length
    if mname =~ /=$/
      if len != 1
        raise ArgumentError, "wrong number of arguments (#{len} for 1)", 
caller(1)
      end
      if self.frozen?
        raise TypeError, "can't modify frozen #{self.class}", caller(1)
      end
      mname.chop!
      @__table__ ||= {}
      @__table__[mname.intern] = args[0]
      self.new_ostruct_member(mname)
    elsif len == 0
      @__table__ ||= {}
      @__table__[mid]
    else
      raise NoMethodError, "undefined method `#{mname}' for #{self}", 
caller(1)
    end
  end

  #
  # Remove the named field from the object.
  #
  def delete_field(name)
    @__table__ ||= {}
    @__table__.delete name.to_sym
  end

  #
  # Returns a string containing a detailed summary of the keys and values.
  #
  def inspect
    str = "<#{self.class}"
    for k,v in (@__table__ ||= {})
      str << " #{k}=#{v.inspect}"
    end
    str << ">"
  end

  def __table__ # :nodoc:
    @__table__ ||= {}
  end
  protected :__table__

  #
  # Compare this object and +other+ for equality.
  #
  def ==(other)
    return false unless(other.kind_of?(OpenStruct))
    return @__table__ == other.table
  end
end

#
# OpenStruct allows you to create data objects and set arbitrary attributes.
# For example:
#
#   require 'ostruct' 
#
#   record = OpenStruct.new
#   record.name    = "John Smith"
#   record.age     = 70
#   record.pension = 300
#   
#   puts record.name     # -> "John Smith"
#   puts record.address  # -> nil
#
# It is like a hash with a different way to access the data.  In fact, it is
# implemented with a hash, and you can initialize it with one.
#
#   hash = { "country" => "Australia", :population => 20_000_000 }
#   data = OpenStruct.new(hash)
#
#   p data        # -> <OpenStruct country="Australia" population=20000000>
#
class OpenStruct
  include OpenStructable
  
  #
  # Create a new OpenStruct object.  The optional +hash+, if given, will
  # generate attributes and values.  For example.
  #
  #   require 'ostruct'
  #   hash = { "country" => "Australia", :population => 20_000_000 }
  #   data = OpenStruct.new(hash)
  #
  #   p data        # -> <OpenStruct country="Australia" population=20000000>
  #
  # By default, the resulting OpenStruct object will have no attributes. 
  #
  def initialize(hash=nil)
    update(hash)
  end
end