public
Rubygem
Description: Mash is a Hash with the ability to read, write, and test for the presence of arbitrary attributes using method calls.
Homepage:
Clone URL: git://github.com/mbleigh/mash.git
mash / lib / mash.rb
100644 228 lines (207 sloc) 6.245 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# 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 (<tt>=</tt>): Sets the attribute of the given method name.
# * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
# * Bang (<tt>!</tt>): 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 = Mash.new
# mash.author!.name = "Michael Bleigh"
# mash.author # => <Mash name="Michael Bleigh">
#
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<Object>:: 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<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