# Mash allows you to create pseudo-objects that have method-like # accessors for hash keys. This is useful for such implementations # as an API-accessing library that wants to fake robust objects # without the overhead of actually doing so. Think of it as OpenStruct # with some additional goodies. # # A Mash will look at the methods you pass it and perform operations # based on the following rules: # # * No punctuation: Returns the value of the hash for that key, or nil if none exists. # * Assignment (=): Sets the attribute of the given method name. # * Existence (?): Returns true or false depending on whether that key has been set. # * Bang (!): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes. # # == Basic Example # # mash = Mash.new # mash.name? # => false # mash.name = "Bob" # mash.name # => "Bob" # mash.name? # => true # # == Hash Conversion Example # # hash = {:a => {:b => 23, :d => {:e => "abc"}}, :f => [{:g => 44, :h => 29}, 12]} # mash = Mash.new(hash) # mash.a.b # => 23 # mash.a.d.e # => "abc" # mash.f.first.g # => 44 # mash.f.last # => 12 # # == Bang Example # # mash = Mash.new # mash.author # => nil # mash.author! # => # # mash = Mash.new # mash.author!.name = "Michael Bleigh" # mash.author # => # class Mash < Hash # If you pass in an existing hash, it will # convert it to a Mash including recursively # descending into arrays and hashes, converting # them as well. def initialize(source_hash = nil, &blk) deep_update(source_hash) if source_hash super(&blk) end class << self; alias [] new; end def id #:nodoc: self["id"] ? self["id"] : super end # Borrowed from Merb's Mash object. # # ==== Parameters # key:: The default value for the mash. Defaults to nil. # # ==== Alternatives # If key is a Symbol and it is a key in the mash, then the default value will # be set to the value matching the key. def default(key = nil) if key.is_a?(Symbol) && key?(key) self[key] else key ? super : super() end end alias_method :regular_reader, :[] alias_method :regular_writer, :[]= # Retrieves an attribute set in the Mash. Will convert # any key passed in to a string before retrieving. def [](key) key = convert_key(key) regular_reader(key) end # Sets an attribute in the Mash. Key will be converted to # a string before it is set. def []=(key,value) #:nodoc: key = convert_key(key) regular_writer(key,convert_value(value)) end # This is the bang method reader, it will return a new Mash # if there isn't a value already assigned to the key requested. def initializing_reader(key) return self[key] if key?(key) self[key] = Mash.new end alias_method :regular_dup, :dup # Duplicates the current mash as a new mash. def dup Mash.new(self) end alias_method :picky_key?, :key? def key?(key) picky_key?(convert_key(key)) end alias_method :regular_inspect, :inspect # Prints out a pretty object-like string of the # defined attributes. def inspect ret = "<#{self.class.to_s}" keys.sort.each do |key| ret << " #{key}=#{self[key].inspect}" end ret << ">" ret end alias_method :to_s, :inspect # Performs a deep_update on a duplicate of the # current mash. def deep_merge(other_hash) dup.deep_merge!(other_hash) end # Recursively merges this mash with the passed # in hash, merging each hash in the hierarchy. def deep_update(other_hash) other_hash = other_hash.to_hash if other_hash.is_a?(Mash) other_hash = other_hash.stringify_keys other_hash.each_pair do |k,v| k = convert_key(k) self[k] = self[k].to_mash if self[k].is_a?(Hash) unless self[k].is_a?(Mash) if self[k].is_a?(Hash) && other_hash[k].is_a?(Hash) self[k] = self[k].deep_merge(other_hash[k]).dup else self.send(k + "=", convert_value(other_hash[k],true)) end end end alias_method :deep_merge!, :deep_update # ==== Parameters # other_hash:: # A hash to update values in the mash with. Keys will be # stringified and Hashes will be converted to Mashes. # # ==== Returns # Mash:: The updated mash. def update(other_hash) other_hash.each_pair do |key, value| if respond_to?(convert_key(key) + "=") self.send(convert_key(key) + "=", convert_value(value)) else regular_writer(convert_key(key), convert_value(value)) end end self end alias_method :merge!, :update # Converts a mash back to a hash (with stringified keys) def to_hash Hash.new(default).merge(self) end def method_missing(method_name, *args) #:nodoc: if (match = method_name.to_s.match(/(.*)=$/)) && args.size == 1 self[match[1]] = args.first elsif (match = method_name.to_s.match(/(.*)\?$/)) && args.size == 0 key?(match[1]) elsif (match = method_name.to_s.match(/(.*)!$/)) && args.size == 0 initializing_reader(match[1]) elsif key?(method_name) self[method_name] elsif match = method_name.to_s.match(/^([a-z][a-z0-9A-Z_]+)$/) default(method_name) else super end end protected def convert_key(key) #:nodoc: key.to_s end def convert_value(value, dup=false) #:nodoc: case value when Hash value = value.dup if value.is_a?(Mash) && dup value.is_a?(Mash) ? value : value.to_mash when Array value.collect{ |e| convert_value(e) } else value end end end class Hash # Returns a new Mash initialized from this Hash. def to_mash mash = Mash.new(self) mash.default = default mash end # Returns a duplicate of the current hash with # all of the keys converted to strings. def stringify_keys dup.stringify_keys! end # Converts all of the keys to strings def stringify_keys! keys.each{|k| v = delete(k) self[k.to_s] = v v.stringify_keys! if v.is_a?(Hash) v.each{|p| p.stringify_keys! if p.is_a?(Hash)} if v.is_a?(Array) } self end end