# require 'singleton' class SafeHash < BasicObject attr_reader :hash alias_method :send, :__send__ def initialize hash = {} reinitialize hash end def []= key, value ::SafeHash.forbidden! key # value = ::SafeHash.new value if value.is_a? ::Hash # @hash[key.to_sym] = value end def set! key, value value = ::SafeHash.new value if value.is_a? ::Hash @hash[key.to_sym] = value end def set *args; raise "you probably mistyped :set! method!" end def include? key @hash.include? key.to_sym end def tap &b b.call self self end def [] key, if_not_exist = ::NotDefined last = key[-1] if last == '!' key = key[0..-2].to_sym if @hash.include? key @hash[key] else raise "no key :#{key}" end elsif last == '?' key = key[0..-2].to_sym @hash.include? key else key = key.to_sym if @hash.include? key @hash[key] elsif if_not_exist == ::NotDefined SafeNil.new key else return if_not_exist end end end def reinitialize hash @hash = {} merge! hash # hash.each do |k, v| # v = ::SafeHash.new v if v.is_a? ::Hash # @hash[k.to_sym] = v # end # @hash.freeze end def method_missing m, obj = ::NotDefined, &b raise "invalid usage, can't pass block to (:#{m})!" if b last = m[-1] if last == '=' ::SafeHash.forbidden! m[0..-2] # self[m[0..-2]] = obj else self[m, obj] end end def to_yaml *args @hash.to_yaml *args end def inspect @hash.inspect end def delete key @hash.delete key.to_sym end def merge! hash, options = {} # parsing arguments deep = options[:deep] || true override = options[:override] || false blank = options[:blank] || false options.validate_options! :deep, :override, :blank raise "invalid options, can't do both :blank and :override simultaneously!" if blank and override # merging hash.each do |k, v| k = k.to_sym if @hash.include? k if deep and (old_v = @hash[k]).respond_to(:is_a_safe_hash?) and v.is_a?(::Hash) old_v.merge! v, options else if blank # do nothing elsif override self.set! k, v else raise "can't override :#{k} config value!" unless override end end else self.set! k, v end end end def merge *args; raise "you probably mistyped :merge! method!" end def merge_if_blank! hash, options = {} options[:blank] = true merge! hash, options end def merge_if_blank *args; raise "you probably mistyped :merge_if_blank method!" end # deep conversion, check and converts nested SafeHashes to Hashes def to_hash options = {} r = {} @hash.each do |k, v| k = k.to_s if options[:to_s] r[k] = if v.respond_to :is_a_safe_hash? v.to_hash options else v end end r end alias_method :to_h, :to_hash def is_a_safe_hash? true end class SafeNil < BasicObject # include ::Singleton def initialize key @key = key end def [] key, if_not_exist = ::NotDefined last = key[-1] if last == '!' raise "no key :#{key}" elsif last == '?' false elsif if_not_exist == ::NotDefined SafeNil.new key else return if_not_exist end end def method_missing m, if_not_exist = ::NotDefined, &b raise "invalid usage, can't pass block to (:#{m})!" if b last = m[-1] if last == '=' ::SafeHash.forbidden! m[0..-2] # raise "No key '#{@key}'!" else self[m, if_not_exist] end end def include? key false end def to_b false end def to_yaml *args nil.to_yaml *args end def to_h {} end def to_s raise "can't convert SafeNil for key '#{@key}' to String!" end def inspect nil.inspect end end class << self def forbidden! key raise "direct modifications of SafeHash is forbidden, use :merge instead (you are trying to assign :#{key} value)!" end end protected def p *a ::Object.send :p, *a end end