# = openobject.rb # # == Copyright (c) 2005 Thomas Sawyer, George Moschovitis # # 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, George Moschovitis # Copyright:: Copyright (c) 2005 Thomas Sawyer, George Moschovitis # License:: Ruby License require 'facets/core/hash/keys_to_sym' require 'facets/core/hash/to_proc' require 'facets/more/basicobject' #require 'facets/more/nullclass' # = OpenObject # # OpenObject is similar to OpenStruct, but differs in a few # significant ways. # # OpenObject is a sublcass of BasicObject, so there are # many fewer reserved Kernel methods in the object's namespace. # # Also it does not create any instance methods for the added entries, # but rather strictly uses a hash. OpenStruct creates methods, but is # actaully inconsitant in their use --try #dup on an OpenStruct and # all the methods disappear. Morevoer, I beleive this feature was added # to OpenStruct for speed, but I have not been able to find this # advantage in benchmark tests. # #-- # Third, when an entry is not found 'null' is returned rather then 'nil'. # This allows for run-on entries withuot error. Eg. # # OpenObject.new.a.b.c #=> null #++ class OpenObject < BasicObject def initialize( init=nil ) case init when Array @table = Hash[*init].keys_to_sym when Hash @table = init.keys_to_sym when NilClass @table = {} when OpenObject @table = init.__table__.dup else @table = init.to_h.keys_to_sym end end def to_openobject ; self ; end def to_a ; @table.values ; end def to_h ; @table.dup ; end def to_hash ; @table.dup ; end # OpenObejct can be converted to an assignment Proc. # # o = OpenObject.new( :a=>1 ) # p = o.to_proc # x = OpenObject.new # p[x] # x.a #=> 1 # def to_proc @table.to_proc end # duplicate def initialize_copy( orig ) super @table = @table.dup end def inspect s = "<#{__class__}" s << " " << @table.collect{|k,v| "#{k}=#{v.inspect}"}.join(' ') unless @table.empty? s << ">" end def marshal_dump @table end def marshal_load( tbl ) @table = tbl end # Compare this object and +other+ for equality. def ==(other) return false unless other.kind_of?( __class__ ) return @table == other.__table__ end def method_missing( sym, *args ) type = sym.to_s[-1,1] name = sym.to_s.gsub(/[=!?]$/, '').to_sym if type == '=' @table[name] = args[0] elsif type == '!' and args.size > 0 @table[name] = args[0] self else if @table.key?(name) @table[name] #elsif @parent # @parent.__send__(name) else nil end end end def [](name) name = name.to_sym #if @table.key?(name) @table[name] #else # Kernel.null #end end def []=(name,val) @table[name.to_sym] = val end # For compatibility with other types of hash-like objects. # Also make OpenObject usable via Enumerator. def each(&yld) @table.each( &yld ) end # Shadow methods # Access to internal table. def __table__ ; @table ; end #-- # TODO Parent feature? Or better only for OpenCascade? #++ #def __parent__ ; @parent ; end #def __parent__=(x) ; @parent = x ; end #def __fetch__(k) ; @table.key?(k.to_sym) ? @table[k.to_sym] : null ; end def __fetch__(k) ; @table[k.to_sym] ; end def __store__(k) ; @table[k.to_sym]=v ; end def __key__?(k) ; @table.key?(k.to_sym) ; end def __keys__ ; @table.keys ; end def __update__( other ) other = Hash[*other] if other.is_a?(Array) other.each{ |k, v| @table[k.to_sym] = v } end def __merge__( other ) o = dup o.__update__( other ) o end # Core Extensions class ::Hash # Convert a Hash into an OpenObject. def to_openobject OpenObject.new( 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 end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin testing require 'test/unit' class TC01 < Test::Unit::TestCase def test_01_001 f0 = OpenObject.new f0[:a] = 1 assert_equal( [1], f0.to_a ) assert_equal( {:a=>1}, f0.to_h ) end def test_01_002 f0 = OpenObject.new( {:a=>1} ) f0[:b] = 2 assert_equal( {:a=>1,:b=>2}, f0.to_h ) end end class TC02 < Test::Unit::TestCase def test_02_001 f0 = OpenObject.new( :f0=>"f0" ) h0 = { :h0=>"h0" } assert_equal( OpenObject.new( :f0=>"f0", :h0=>"h0" ), f0.__merge__( h0 ) ) assert_equal( {:f0=>"f0", :h0=>"h0"}, h0.merge( f0 ) ) end def test_02_002 f1 = OpenObject.new( :f1=>"f1" ) h1 = { :h1=>"h1" } f1.__update__( h1 ) h1.update( f1 ) assert_equal( OpenObject.new( :f1=>"f1", :h1=>"h1" ), f1 ) assert_equal( {:f1=>"f1", :h1=>"h1"}, h1 ) end end class TC03 < Test::Unit::TestCase def test_03_001 fo = OpenObject.new 99.times{ |i| fo.__send__( "n#{i}=", 1 ) } 99.times{ |i| assert_equal( 1, fo.__send__( "n#{i}" ) ) } end end class TC04 < Test::Unit::TestCase def test_04_001 p = lambda { |x| x.word = "Hello" } o = p.to_openobject assert_equal( "Hello", o.word ) end end =end