Improving handling of reserved names in OpenStruct

 OpenStruct の実装を何点か改善したいと考えています。

  1. [ruby-dev:40463]のような respond_to? の挙動に依存した部分は
    書き換えたい。protected メソッドと同名のメンバーは従来通り
    使用可能に。

  2. new_ostruct_member など、 OpenStruct の実装に使われている
    public メソッドと同名のメンバーを作ろうとしても、値を取り
    出そうとすれば予期せぬメソッドが呼ばれ、また仮に代入できて
    しまうとすれば誤動作を招くのだから、 NameError を出すべき
    ではないだろうか。

    ただ、無視してくれた方が不定データを取り込む際には便利かも
    しれないので、明示的にチェックの有無を指定させる方がよい?
    たとえば OpenStruct.new(hash, validate = false) とフラグを
    åŠ ãˆã‚‹ãªã©ã€‚

  3. class や method などの Kernel モジュールメソッドと同名の
    メンバーは、ユーザが望むなら使えるべきではないか。

 関心のある方は意見をください。

 とりあえず上記の基本部分をひとまとめにしたパッチを付けます。


Akinori MUSHA / http://akinori.org/

Index: lib/ostruct.rb

— lib/ostruct.rb (revision 26742)
+++ lib/ostruct.rb (working copy)
@@ -31,6 +31,11 @@

p data # → <OpenStruct country=“Australia”

population=20000000>

class OpenStruct

  • METHOD_class = Kernel.instance_method(:class)
  • METHOD_object_id = Kernel.instance_method(:object_id)
  • METHOD_method = Kernel.instance_method(:method)
  • METHOD_public_method = Kernel.instance_method(:public_method)
  • Create a new OpenStruct object. The optional +hash+, if given,

will

generate attributes and values. For example.

@@ -79,12 +84,25 @@ class OpenStruct

def new_ostruct_member(name)
name = name.to_sym

  • unless self.respond_to?(name)
  •  class << self; self; end.class_eval do
    
  •    define_method(name) { @table[name] }
    
  •    define_method("#{name}=") { |x| modifiable[name] = x }
    
  • singleton = class << self; self; end
  • if self.respond_to?(name)
  •  protected_p = false
    
  •  begin
    
  •    owner = METHOD_public_method.bind(self).call(name).owner
    
  •  rescue NameError
    
  •    owner = METHOD_method.bind(self).call(name).owner
    
  •    protected_p = true
    
  •  end
    
  •  if owner == singleton || protected_p
    
  •    return name
    
  •  elsif owner <= OpenStruct
    
  •    raise NameError, "the name `#{name}' is reserved by 
    

implementation (#{owner})"
end
end

  • singleton.class_eval do
  •  define_method(name) { @table[name] }
    
  •  define_method("#{name}=") { |x| modifiable[name] = x }
    
  • end
    name
    end

@@ -116,14 +134,14 @@ class OpenStruct

Returns a string containing a detailed summary of the keys and

values.

def inspect

  • str = “#<#{self.class}”
  • str = “#<#{METHOD_class.bind(self).call}”

    ids = (Thread.current[InspectKey] ||= [])
    if ids.include?(object_id)
    return str << ’ …>’
    end

  • ids << object_id
  • ids << METHOD_object_id.bind(self).call
    begin
    first = true
    for k,v in @table
    Index: test/ostruct/test_ostruct.rb
    ===================================================================
    — test/ostruct/test_ostruct.rb (revision 26742)
    +++ test/ostruct/test_ostruct.rb (working copy)
    @@ -21,6 +21,47 @@ class TC_OpenStruct < Test::Unit::TestCa
    assert_not_equal(o1, o2)
    end

  • def test_kernel_methods

  • o = OpenStruct.new

  • assert_nothing_raised(“The name ‘method’ should be available”) {

  •  o.method = :something
    
  •  assert_equal(:something, o.method)
    
  • }

  • o = OpenStruct.new

  • o.object_id = 123

  • o.class = “Foo”

  • assert_equal(“#<OpenStruct object_id=123, class="Foo">”,
    o.inspect,

  •  "Setting 'class' should not affect the result of #inspect")
    
  • end

  • def test_protected_methods

  • o = OpenStruct.new

  • assert_nothing_raised(“The name ‘modifiable’ should be available”)
    {

  •  o.modifiable = :something
    
  •  assert_equal(:something, o.modifiable)
    
  • }

  • assert_nothing_raised(“The name ‘table’ should be available”) {

  •  o.table = :something
    
  •  assert_equal(:something, o.table)
    
  • }

  • end

  • def test_reserved_methods

  • assert_raises(NameError) {

  •  o = OpenStruct.new
    
  •  o.inspect = "foo"
    
  • }

  • assert_raises(NameError) {

  •  subclass = Class.new(OpenStruct)
    
  •  o = subclass.new
    
  •  o.inspect = "foo"
    
  • }

  • end

  • def test_inspect
    foo = OpenStruct.new
    assert_equal(“#”, foo.inspect)