module Faster # ## Faster::OpenStruct # # Up to 40 (!) times more memory efficient version of OpenStruct # # Differences from Ruby MRI OpenStruct: # # 1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored) # # 2. Doesn't convert hash keys to symbols (by default string keys are used, # with fallback to symbol keys) # # 3. Creates methods on the fly on `OpenStruct` class, instead of singleton class. # Uses `module_eval` with string to avoid holding scope references for every method. # # 4. Refactored, crud clean, spec covered :) # class OpenStruct # Undefine particularly nasty interfering methods on Ruby 1.8 undef :type if method_defined?(:type) undef :id if method_defined?(:id) def initialize(hash = nil) @hash = hash || {} @initialized_empty = hash == nil end def method_missing(method_name_sym, *args) if method_name_sym.to_s[-1] == ?= if args.size != 1 raise ArgumentError, "wrong number of arguments (#{args.size} for 1)", caller(1) end if self.frozen? raise TypeError, "can't modify frozen #{self.class}", caller(1) end __new_ostruct_member__(method_name_sym.to_s.chomp("=")) send(method_name_sym, args[0]) elsif args.size == 0 __new_ostruct_member__(method_name_sym) send(method_name_sym) else raise NoMethodError, "undefined method `#{method_name_sym}' for #{self}", caller(1) end end def __new_ostruct_member__(method_name_sym) self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 def #{ method_name_sym } @hash.fetch("#{ method_name_sym }", @hash[:#{ method_name_sym }]) # read by default from string key, then try symbol # if string key doesn't exist end END_EVAL unless method_name_sym.to_s[-1] == ?? # can't define writer for predicate method self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 def #{ method_name_sym }=(val) if @hash.key?("#{ method_name_sym }") || @initialized_empty # write by default to string key (when it is present # in initialization hash or initialization hash # wasn't provided) @hash["#{ method_name_sym }"] = val # if it doesn't exist - write to symbol key else @hash[:#{ method_name_sym }] = val end end END_EVAL end end def empty? @hash.empty? end # # Compare this object and +other+ for equality. # def ==(other) return false unless other.is_a?(self.class) @hash == other.instance_variable_get(:@hash) end InspectKey = :__inspect_key__ # :nodoc: # # Returns a string containing a detailed summary of the keys and values. # def inspect str = "#<#{ self.class }" str << " #{ @hash.map { |k, v| "#{ k }=#{ v.inspect }" }.join(", ") }" unless @hash.empty? str << ">" end def inspect_with_reentrant_guard(default = "...") Thread.current[InspectKey] ||= [] if Thread.current[InspectKey].include?(self) return default # reenter detected end Thread.current[InspectKey] << self begin inspect_without_reentrant_guard ensure Thread.current[InspectKey].pop end end alias_method :inspect_without_reentrant_guard, :inspect alias_method :inspect, :inspect_with_reentrant_guard alias :to_s :inspect end end