# = OpenObject # # OpenObject is very similar to Ruby's own OpenStruct, but it offers some # useful advantages. With OpenStruct slots with the same name's as predefined # Object methods can not be used. With OpenObject any slot can be defined, # OpendObject is also a bit faster becuase it is based on a Hash, not # method signitures. # # Because OpenObject is a subclass of Hash, it can do just about # everything a Hash can do, except that most public methods have been # made protected and thus available only internally or via #send. # # 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 aware of this. OpenObject should be used in well # controlled scenarios. # # If you wish to pass an OpenObject to a routine that normal takes a Hash, # but are uncertain it can handle the distictions properly you can convert # easily to a Hash using #to_hash! and the result will automatically be # converted back to an OpenObject on return. # # 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. # # == Authors # # * Thomas Sawyer # * George Moschovitis # # == Copying # # 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. require 'facets/hash/to_h' require 'facets/hash/to_proc' require 'facets/kernel/object_class' require 'facets/kernel/object_hexid' # = OpenObject # # OpenObject is very similar to Ruby's own OpenStruct, but it offers some # useful advantages. With OpenStruct slots with the same name's as predefined # Object methods can not be used. With OpenObject any slot can be defined, # OpendObject is also a bit faster becuase it is based on a Hash, not # method signitures. # # Because OpenObject is a subclass of Hash, it can do just about # everything a Hash can do, except that most public methods have been # made protected and thus available only internally or via #send. # # 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 aware of this. OpenObject should be used in well # controlled scenarios. # # If you wish to pass an OpenObject to a routine that normal takes a Hash, # but are uncertain it can handle the distictions properly you can convert # easily to a Hash using #to_hash! and the result will automatically be # converted back to an OpenObject on return. # # 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