# = openobject.rb # # == Copyright (c) 2005,2006 Thomas Sawyer # # Ruby License # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # == Authors and Contributors # # * Thomas Sawyer # * George Moschovitis # Author:: Thomas Sawyer # Copyright:: Copyright (c) 2005 Thomas Sawyer # License:: Ruby License require 'facets/core/hash/to_h' require 'facets/core/hash/to_proc' require 'facets/core/kernel/object_class' require 'facets/core/kernel/object_hexid' # = OpenObject # # OpenObject is similar to OpenStruct, but differs in a couple ways. # # OpenObject is a subclass of Hash and can do just about everything # a Hash can do, except that most public methods have been made # protected and thus only available internally or via #send. # A small number, like #each are still exposed publically though # b/c of their importance. # # OpenObject will also clobber any method for which a slot is defined. # Even generally very important methods can be clobbered, like # instance_eval. So be careful. OpenObject should be used in highly # controlled scenarios. If you want to pass one off to an "unknown" # rountine, it is best to convert it to a Hash first and convert it # back to an OpenObject when finished. To facilitate this the method # #as_hash! is provided. # # o = OpenObject.new(:a=>1,:b=>2) # o.as_hash!{ |h| h.update(:a=>6) } # o #=> #6,:b=>2}> # # Finally, unlike a regular Hash, all OpenObject's keys are symbols and # all keys are converted to such using #to_sym on the fly. class OpenObject < Hash PUBLIC_METHODS = /(^__|^instance_|^object_|^\W|^as$|^send$|^class$|\?$)/ protected *public_instance_methods.select{ |m| m !~ PUBLIC_METHODS } def self.[](hash=nil) new(hash) end # Inititalizer for OpenObject is slightly differnt than that of Hash. # It does not take a default parameter, but an initial priming Hash # as with OpenStruct. The initializer can still take a default block # however. To set the degault value use ++#default!(value)++. # # OpenObject(:a=>1).default!(0) def initialize( hash=nil, &yld ) super( &yld ) hash.each { |k,v| define_slot(k,v) } if hash end def initialize_copy( orig ) orig.each { |k,v| define_slot(k,v) } end # Object inspection. (Careful, this can be clobbered!) def inspect "#<#{object_class}:#{object_hexid} #{super}>" end # Conversion methods. (Careful, these can be clobbered!) def to_a() super end def to_h() {}.update(self) end def to_hash() {}.update(self) end def to_proc() super end def to_openobject() self end # Iterate over each key-value pair. (Careful, this can be clobbered!) def each(&yld) super(&yld) end # Merge one OpenObject with another creating a new OpenObject. def merge( other ) d = dup d.send(:update, other) d end # Update this OpenObject with another. def update( other ) begin other.each { |k,v| define_slot(k,v) } rescue other = other.to_h retry end end # def delete(key) super(key.to_sym) end # Set the default value. def default!(default) self.default = default end # Preform inplace action on OpenObject as if it were a regular Hash. #-- # TODO Not so sure about #as_hash!. For starters if it doesn't return a hash it will fail. # TODO Replace by using #as(Hash). Perhaps as_hash and as_object shortcuts? Why? #++ def as_hash!(&yld) replace(yld.call(to_hash)) end # Check equality. (Should equal be true for Hash too?) def ==( other ) return false unless OpenObject === other super(other) #(other.send(:table)) end def []=(k,v) protect_slot(k) super(k.to_sym,v) end def [](k) super(k.to_sym) end protected def store(k,v) super(k.to_sym,v) define_slot(k) end def fetch(k,*d,&b) super(k.to_sym,*d,&b) end def define_slot( key, value=nil ) protect_slot( key ) self[key.to_sym] = value end def protect_slot( key ) (class << self; self; end).class_eval { protected key rescue nil } end def method_missing( sym, arg=nil, &blk) type = sym.to_s[-1,1] key = sym.to_s.sub(/[=?!]$/,'').to_sym if type == '=' define_slot(key,arg) elsif type == '!' define_slot(key,arg) self else self[key] end end end # Core Extensions class NilClass # Nil converts to an empty OpenObject. def to_openobject OpenObject.new end end class Hash # Convert a Hash into an OpenObject. def to_openobject OpenObject[self] end end class Proc # Translates a Proc into an OpenObject. By droping an OpenObject into # the Proc, the resulting assignments incured as the procedure is # evaluated produce the OpenObject. This technique is simlar to that # of MethodProbe. # # p = lambda { |x| # x.word = "Hello" # } # o = p.to_openobject # o.word #=> "Hello" # # NOTE The Proc must have an arity of one --no more and no less. def to_openobject raise ArgumentError, 'bad arity for converting Proc to openobject' if arity != 1 o = OpenObject.new self.call( o ) o end end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin testing require 'test/unit' class TestOpenObject1 < Test::Unit::TestCase def test_1_01 o = OpenObject.new assert( o.respond_to?(:key?) ) end def test_1_02 assert_instance_of( OpenObject, OpenObject[{}] ) end def test_1_03 f0 = OpenObject.new f0[:a] = 1 #assert_equal( [1], f0.to_a ) assert_equal( {:a=>1}, f0.to_h ) end def test_1_04 f0 = OpenObject[:a=>1] f0[:b] = 2 assert_equal( {:a=>1,:b=>2}, f0.to_h ) end def test_1_05 f0 = OpenObject[:class=>1] assert_equal( 1, f0.class ) end end class TestOpenObject2 < Test::Unit::TestCase def test_2_01 f0 = OpenObject[:f0=>"f0"] h0 = { :h0=>"h0" } assert_equal( OpenObject[:f0=>"f0", :h0=>"h0"], f0.send(:merge,h0) ) assert_equal( {:f0=>"f0", :h0=>"h0"}, h0.merge( f0 ) ) end def test_2_02 f1 = OpenObject[:f1=>"f1"] h1 = { :h1=>"h1" } f1.send(:update,h1) h1.update( f1 ) assert_equal( OpenObject[:f1=>"f1", :h1=>"h1"], f1 ) assert_equal( {:f1=>"f1", :h1=>"h1"}, h1 ) end def test_2_03 o = OpenObject[:a=>1,:b=>{:x=>9}] assert_equal( 9, o[:b][:x] ) assert_equal( 9, o.b[:x] ) end def test_2_04 o = OpenObject["a"=>1,"b"=>{:x=>9}] assert_equal( 1, o["a"] ) assert_equal( 1, o[:a] ) assert_equal( {:x=>9}, o["b"] ) assert_equal( {:x=>9}, o[:b] ) assert_equal( 9, o["b"][:x] ) assert_equal( nil, o[:b]["x"] ) end end class TestOpenObject3 < Test::Unit::TestCase def test_3_01 fo = OpenObject.new 9.times{ |i| fo.send( "n#{i}=", 1 ) } 9.times{ |i| assert_equal( 1, fo.send( "n#{i}" ) ) } end end class TestOpenObject4 < Test::Unit::TestCase def test_4_01 ho = {} fo = OpenObject.new 5.times{ |i| ho["n#{i}".to_sym]=1 } 5.times{ |i| fo.send( "n#{i}=", 1 ) } assert_equal(ho, fo.to_h) end end class TestOpenObject5 < Test::Unit::TestCase def test_5_01 p = lambda { |x| x.word = "Hello" } o = p.to_openobject assert_equal( "Hello", o.word ) end def test_5_02 p = lambda { |x| x.word = "Hello" } o = OpenObject[:a=>1,:b=>2] assert_instance_of( Proc, o.to_proc ) end end =end