summaryrefslogtreecommitdiff
path: root/lib/hashie/extensions/method_access.rb
blob: cf13da0be09a36d11ddbf3181b571ba4a52b5345 (plain)
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
module Hashie
  module Extensions
    # MethodReader allows you to access keys of the hash
    # via method calls. This gives you an OStruct like way
    # to access your hash's keys. It will recognize keys
    # either as strings or symbols.
    #
    # Note that while nil keys will be returned as nil,
    # undefined keys will raise NoMethodErrors. Also note that
    # #respond_to? has been patched to appropriately recognize
    # key methods.
    #
    # @example
    #   class User < Hash
    #     include Hashie::Extensions::MethodReader
    #   end
    #
    #   user = User.new
    #   user['first_name'] = 'Michael'
    #   user.first_name # => 'Michael'
    #
    #   user[:last_name] = 'Bleigh'
    #   user.last_name # => 'Bleigh'
    #
    #   user[:birthday] = nil
    #   user.birthday # => nil
    #
    #   user.not_declared # => NoMethodError
    module MethodReader
      def respond_to_missing?(name, include_private = false)
        return true if key?(name.to_s) || key?(name.to_sym)
        super
      end

      def method_missing(name, *args)
        if key?(name)
          self[name]
        else
          sname = name.to_s
          if key?(sname)
            self[sname]
          elsif sname[-1] == '?'
            kname = sname[0..-2]
            key?(kname) || key?(kname.to_sym)
          else
            super
          end
        end
      end
    end

    # MethodWriter gives you #key_name= shortcuts for
    # writing to your hash. Keys are written as strings,
    # override #convert_key if you would like to have symbols
    # or something else.
    #
    # Note that MethodWriter also overrides #respond_to such
    # that any #method_name= will respond appropriately as true.
    #
    # @example
    #   class MyHash < Hash
    #     include Hashie::Extensions::MethodWriter
    #   end
    #
    #   h = MyHash.new
    #   h.awesome = 'sauce'
    #   h['awesome'] # => 'sauce'
    #
    module MethodWriter
      def respond_to_missing?(name, include_private = false)
        return true if name.to_s =~ /=$/
        super
      end

      def method_missing(name, *args)
        return self[convert_key(Regexp.last_match[1])] = args.first if args.size == 1 && name.to_s =~ /(.*)=$/

        super
      end

      def convert_key(key)
        key.to_s
      end
    end

    # MethodQuery gives you the ability to check for the truthiness
    # of a key via method calls. Note that it will return false if
    # the key is set to a non-truthful value, not if the key isn't
    # set at all. Use #key? for checking if a key has been set.
    #
    # MethodQuery will check against both string and symbol names
    # of the method for existing keys. It also patches #respond_to
    # to appropriately detect the query methods.
    #
    # @example
    #   class MyHash < Hash
    #     include Hashie::Extensions::MethodQuery
    #   end
    #
    #   h = MyHash.new
    #   h['abc'] = 123
    #   h.abc? # => true
    #   h['def'] = nil
    #   h.def? # => false
    #   h.hji? # => NoMethodError
    module MethodQuery
      def respond_to_missing?(name, include_private = false)
        if query_method?(name) && indifferent_key?(key_from_query_method(name))
          true
        else
          super
        end
      end

      def method_missing(name, *args)
        return super unless args.empty?

        if query_method?(name)
          key = key_from_query_method(name)
          if indifferent_key?(key)
            !!(self[key] || self[key.to_sym])
          else
            super
          end
        else
          super
        end
      end

      private

      def indifferent_key?(name)
        name = name.to_s
        key?(name) || key?(name.to_sym)
      end

      def key_from_query_method(query_method)
        query_method.to_s[0..-2]
      end

      def query_method?(name)
        name.to_s.end_with?('?')
      end
    end

    # A macro module that will automatically include MethodReader,
    # MethodWriter, and MethodQuery, giving you the ability to read,
    # write, and query keys in a hash using method call shortcuts.
    module MethodAccess
      def self.included(base)
        [MethodReader, MethodWriter, MethodQuery].each do |mod|
          base.send :include, mod
        end
      end
    end

    # A module shared between MethodOverridingWriter and MethodOverridingInitializer
    # to contained shared logic. This module aids in redefining existing hash methods.
    module RedefineMethod
      protected

      def method?(name)
        methods.map(&:to_s).include?(name)
      end

      def redefine_method(method_name)
        eigenclass = class << self; self; end
        eigenclass.__send__(:alias_method, "__#{method_name}", method_name)
        eigenclass.__send__(:define_method, method_name, -> { self[method_name] })
      end
    end

    # MethodOverridingWriter gives you #key_name= shortcuts for
    # writing to your hash. It allows methods to be overridden by
    # #key_name= shortcuts and aliases those methods with two
    # leading underscores.
    #
    # Keys are written as strings. Override #convert_key if you
    # would like to have symbols or something else.
    #
    # Note that MethodOverridingWriter also overrides
    # #respond_to_missing? such that any #method_name= will respond
    # appropriately as true.
    #
    # @example
    #   class MyHash < Hash
    #     include Hashie::Extensions::MethodOverridingWriter
    #   end
    #
    #   h = MyHash.new
    #   h.awesome = 'sauce'
    #   h['awesome'] # => 'sauce'
    #   h.zip = 'a-dee-doo-dah'
    #   h.zip # => 'a-dee-doo-dah'
    #   h.__zip # => [[['awesome', 'sauce'], ['zip', 'a-dee-doo-dah']]]
    #
    module MethodOverridingWriter
      include RedefineMethod

      def convert_key(key)
        key.to_s
      end

      def method_missing(name, *args)
        if args.size == 1 && name.to_s =~ /(.*)=$/
          key = Regexp.last_match[1]
          redefine_method(key) if method?(key) && !already_overridden?(key)
          return self[convert_key(key)] = args.first
        end

        super
      end

      def respond_to_missing?(name, include_private = false)
        return true if name.to_s.end_with?('=')
        super
      end

      protected

      def already_overridden?(name)
        method?("__#{name}")
      end
    end

    # A macro module that will automatically include MethodReader,
    # MethodOverridingWriter, and MethodQuery, giving you the ability
    # to read, write, and query keys in a hash using method call
    # shortcuts that can override object methods. Any overridden
    # object method is automatically aliased with two leading
    # underscores.
    module MethodAccessWithOverride
      def self.included(base)
        [MethodReader, MethodOverridingWriter, MethodQuery, MethodOverridingInitializer].each do |mod|
          base.send :include, mod
        end
      end
    end

    # MethodOverridingInitializer allows you to override default hash
    # methods when passing in values from an existing hash. The overriden
    # methods are aliased with two leading underscores.
    #
    # @example
    #   class MyHash < Hash
    #     include Hashie::Extensions::MethodOverridingInitializer
    #   end
    #
    #   h = MyHash.new(zip: 'a-dee-doo-dah')
    #   h.zip # => 'a-dee-doo-dah'
    #   h.__zip # => [[['zip', 'a-dee-doo-dah']]]
    module MethodOverridingInitializer
      include RedefineMethod

      def initialize(hash = {})
        hash.each do |key, value|
          skey = key.to_s
          redefine_method(skey) if method?(skey)
          self[skey] = value
        end
      end
    end
  end
end