gorillib 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. data/CHANGELOG.textile +6 -0
  2. data/README.textile +34 -11
  3. data/VERSION +1 -1
  4. data/gorillib.gemspec +63 -5
  5. data/lib/gorillib/enumerable/sum.rb +2 -2
  6. data/lib/gorillib/hash/compact.rb +2 -29
  7. data/lib/gorillib/hash/deep_compact.rb +2 -12
  8. data/lib/gorillib/hash/deep_dup.rb +4 -0
  9. data/lib/gorillib/hash/deep_merge.rb +2 -14
  10. data/lib/gorillib/hash/indifferent_access.rb +207 -0
  11. data/lib/gorillib/hash/keys.rb +2 -40
  12. data/lib/gorillib/hash/reverse_merge.rb +2 -24
  13. data/lib/gorillib/hash/slice.rb +2 -51
  14. data/lib/gorillib/hash/tree_merge.rb +4 -0
  15. data/lib/gorillib/hashlike.rb +824 -0
  16. data/lib/gorillib/hashlike/compact.rb +60 -0
  17. data/lib/gorillib/hashlike/deep_compact.rb +18 -0
  18. data/lib/gorillib/hashlike/deep_dup.rb +15 -0
  19. data/lib/gorillib/hashlike/deep_merge.rb +20 -0
  20. data/lib/gorillib/hashlike/hashlike_via_accessors.rb +169 -0
  21. data/lib/gorillib/hashlike/keys.rb +59 -0
  22. data/lib/gorillib/hashlike/reverse_merge.rb +31 -0
  23. data/lib/gorillib/hashlike/slice.rb +67 -0
  24. data/lib/gorillib/hashlike/tree_merge.rb +76 -0
  25. data/lib/gorillib/metaprogramming/mattr_accessor.rb +1 -1
  26. data/lib/gorillib/receiver.rb +315 -0
  27. data/lib/gorillib/receiver/active_model_shim.rb +19 -0
  28. data/lib/gorillib/receiver/acts_as_hash.rb +191 -0
  29. data/lib/gorillib/receiver/acts_as_loadable.rb +42 -0
  30. data/lib/gorillib/receiver/tree_diff.rb +74 -0
  31. data/lib/gorillib/receiver/validations.rb +30 -0
  32. data/lib/gorillib/struct/acts_as_hash.rb +108 -0
  33. data/lib/gorillib/struct/hashlike_iteration.rb +0 -0
  34. data/notes/fancy_hashes_and_receivers.textile +120 -0
  35. data/notes/hash_rdocs.textile +97 -0
  36. data/spec/hash/deep_merge_spec.rb +0 -2
  37. data/spec/hash/indifferent_access_spec.rb +391 -0
  38. data/spec/hash/slice_spec.rb +35 -12
  39. data/spec/hashlike/behave_same_as_hash_spec.rb +105 -0
  40. data/spec/hashlike/hashlike_behavior_spec.rb +824 -0
  41. data/spec/hashlike/hashlike_via_accessors_fuzzing_spec.rb +37 -0
  42. data/spec/hashlike/hashlike_via_accessors_spec.rb +262 -0
  43. data/spec/hashlike_spec.rb +302 -0
  44. data/spec/metaprogramming/aliasing_spec.rb +3 -0
  45. data/spec/metaprogramming/cattr_accessor_spec.rb +2 -0
  46. data/spec/metaprogramming/class_attribute_spec.rb +2 -0
  47. data/spec/metaprogramming/delegation_spec.rb +2 -0
  48. data/spec/metaprogramming/mattr_accessor_spec.rb +2 -0
  49. data/spec/metaprogramming/singleton_class_spec.rb +3 -0
  50. data/spec/receiver/acts_as_hash_spec.rb +286 -0
  51. data/spec/receiver_spec.rb +478 -0
  52. data/spec/spec_helper.rb +11 -6
  53. data/spec/string/truncate_spec.rb +1 -0
  54. data/spec/struct/acts_as_hash_fuzz_spec.rb +67 -0
  55. data/spec/struct/acts_as_hash_spec.rb +426 -0
  56. data/spec/support/hashlike_fuzzing_helper.rb +127 -0
  57. data/spec/support/hashlike_helper.rb +75 -0
  58. data/spec/support/hashlike_struct_helper.rb +37 -0
  59. data/spec/support/hashlike_via_delegation.rb +30 -0
  60. data/spec/support/matchers/be_array_eql.rb +12 -0
  61. data/spec/support/matchers/be_hash_eql.rb +14 -0
  62. data/spec/support/matchers/enumerate_method.rb +10 -0
  63. data/spec/support/matchers/evaluate_to_true.rb +5 -0
  64. metadata +62 -4
@@ -0,0 +1,76 @@
1
+ module Gorillib
2
+ module Hashlike
3
+ module TreeMerge
4
+
5
+ # Recursively merges using receive
6
+ #
7
+ # Modifies the full receiver chain in-place.
8
+ #
9
+ # For each key in keys,
10
+ # * if self's value is nil, receive the attribute.
11
+ # * if self's attribute is an Array, append to it.
12
+ # * if self's value responds to tree_merge!, tree merge it.
13
+ # * if self's value responds_to merge!, merge! it.
14
+ # * otherwise, receive the value from other_hash
15
+ #
16
+ def tree_merge!(other_hash)
17
+ keys.each do |key|
18
+ # get other's val if any
19
+ if other_hash.has_key?(key.to_sym) then other_val = other_hash[key.to_sym]
20
+ elsif other_hash.has_key?(key.to_s) then other_val = other_hash[key.to_s]
21
+ else next ; end
22
+ #
23
+ self_val = self[key]
24
+ # p ['receiver tree_merge', key, self_val.respond_to?(:tree_merge!), self[key], other_val]
25
+ case
26
+ when other_val.nil? then next
27
+ when (not has_key?(key)) then _receive_attr(key, other_val)
28
+ when receiver_attrs[key][:merge_as] == :hash_of_arrays
29
+ self_val.merge!(other_val) do |k, v1, v2| case when v1.blank? then v2 when v2.blank? then v1 else v1 + v2 end end
30
+ when self_val.is_a?(Array) then self[key] += other_val
31
+ when self_val.respond_to?(:tree_merge!) then self[key] = self_val.tree_merge!(other_val)
32
+ when self_val.respond_to?(:merge!) then self[key] = self_val.merge!(other_val)
33
+ else _receive_attr(key, other_val)
34
+ end
35
+ end
36
+ run_after_receivers(other_hash)
37
+ self
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+
44
+ class Hash
45
+ # Recursively merges using receive
46
+ #
47
+ # Modifies the full receiver chain in-place.
48
+ #
49
+ # For each key in keys,
50
+ # * if self's value is nil, receive the attribute.
51
+ # * if self's attribute is an Array, append to it.
52
+ # * if self's value responds to tree_merge!, deep merge it.
53
+ # * if self's value responds_to merge!, merge! it.
54
+ # * otherwise, receive the value from other_hash
55
+ #
56
+ def tree_merge!(other_hash)
57
+ [self.keys, other_hash.keys].flatten.uniq.each do |key|
58
+ # get other's val if any
59
+ if other_hash.has_key?(key.to_sym) then other_val = other_hash[key.to_sym]
60
+ elsif other_hash.has_key?(key.to_s) then other_val = other_hash[key.to_s]
61
+ else next ; end
62
+ #
63
+ self_val = self[key]
64
+ # p ['hash tree_merge', key, self_val.respond_to?(:tree_merge!), self_val, other_val]
65
+ case
66
+ when other_val.nil? then next
67
+ when (not has_key?(key)) then self[key] = other_val
68
+ when self_val.is_a?(Array) then self[key] += other_val
69
+ when self_val.respond_to?(:tree_merge!) then self[key] = self_val.tree_merge!(other_val)
70
+ when self_val.respond_to?(:merge!) then self[key] = self_val.merge!(other_val)
71
+ else self[key] = other_val
72
+ end
73
+ end
74
+ self
75
+ end
76
+ end
@@ -57,5 +57,5 @@ class Module
57
57
  mattr_reader(*syms)
58
58
  mattr_writer(*syms)
59
59
  end unless method_defined?(:mattr_accessor)
60
-
60
+
61
61
  end
@@ -0,0 +1,315 @@
1
+ # dummy type for receiving True or False
2
+ class Boolean ; end unless defined?(Boolean)
3
+
4
+ # Receiver lets you describe complex (even recursive!) actively-typed data models that
5
+ # * are creatable or assignable from static data structures
6
+ # * perform efficient type conversion when assigning from a data structure,
7
+ # * but with nothing in the way of normal assignment or instantiation
8
+ # * and no requirements on the initializer
9
+ #
10
+ # class Tweet
11
+ # include Receiver
12
+ # rcvr_accessor :id, Integer
13
+ # rcvr_accessor :user_id, Integer
14
+ # rcvr_accessor :created_at, Time
15
+ # end
16
+ # p Tweet.receive(:id => "7", :user_id => 9, :created_at => "20101231010203" )
17
+ # # => #<Tweet @id=7, @user_id=9, @created_at=2010-12-31 07:02:03 UTC>
18
+ #
19
+ # You can override receive behavior in a straightforward and predictable way:
20
+ #
21
+ # class TwitterUser
22
+ # include Receiver
23
+ # rcvr_accessor :id, Integer
24
+ # rcvr_accessor :screen_name, String
25
+ # rcvr_accessor :follower_ids, Array, :of => Integer
26
+ # # accumulate unique follower ids
27
+ # def receive_follower_ids(arr)
28
+ # @follower_ids = (@follower_ids||[]) + arr.map(&:to_i)
29
+ # @follower_ids.uniq!
30
+ # end
31
+ # end
32
+ #
33
+ # The receiver pattern works naturally with inheritance:
34
+ #
35
+ # class TweetWithUser < Tweet
36
+ # rcvr_accessor :user, TwitterUser
37
+ # after_receive do |hsh|
38
+ # self.user_id = self.user.id if self.user
39
+ # end
40
+ # end
41
+ # p TweetWithUser.receive(:id => 8675309, :created_at => "20101231010203", :user => { :id => 24601, :screen_name => 'bob', :follower_ids => [1, 8, 3, 4] })
42
+ # => #<TweetWithUser @id=8675309, @created_at=2010-12-31 07:02:03 UTC, @user=#<TwitterUser @id=24601, @screen_name="bob", @follower_ids=[1, 8, 3, 4]>, @user_id=24601>
43
+ #
44
+ # TweetWithUser was able to add another receiver, applicable only to itself and its subclasses.
45
+ #
46
+ # The receive method works well with sparse data -- you can accumulate
47
+ # attributes without trampling formerly set values:
48
+ #
49
+ # tw = Tweet.receive(:id => "7", :user_id => 9 )
50
+ # p tw
51
+ # # => #<Tweet @id=7, @user_id=9>
52
+ #
53
+ # tw.receive!(:created_at => "20101231010203" )
54
+ # p tw
55
+ # # => #<Tweet @id=7, @user_id=9, @created_at=2010-12-31 07:02:03 UTC>
56
+ #
57
+ # Note the distinction between an explicit nil field and a missing field:
58
+ #
59
+ # tw.receive!(:user_id => nil, :created_at => "20090506070809" )
60
+ # p tw
61
+ # # => #<Tweet @id=7, @user_id=nil, @created_at=2009-05-06 12:08:09 UTC>
62
+ #
63
+ # There are helpers for default and required attributes:
64
+ #
65
+ # class Foo
66
+ # include Receiver
67
+ # rcvr_accessor :is_reqd, String, :required => true
68
+ # rcvr_accessor :also_reqd, String, :required => true
69
+ # rcvr_accessor :has_default, String, :default => 'hello'
70
+ # end
71
+ # foo_obj = Foo.receive(:is_reqd => "hi")
72
+ # # => #<Foo:0x00000100bd9740 @is_reqd="hi" @has_default="hello">
73
+ # foo_obj.missing_attrs
74
+ # # => [:also_reqd]
75
+ #
76
+ module Receiver
77
+
78
+ RECEIVER_BODIES = {} unless defined?(RECEIVER_BODIES)
79
+ RECEIVER_BODIES[Symbol] = %q{ v.blank? ? nil : v.to_sym }
80
+ RECEIVER_BODIES[Integer] = %q{ v.blank? ? nil : v.to_i }
81
+ RECEIVER_BODIES[Float] = %q{ v.blank? ? nil : v.to_f }
82
+ RECEIVER_BODIES[String] = %q{ v.to_s }
83
+ RECEIVER_BODIES[Time] = %q{ v.nil? ? nil : Time.parse(v.to_s).utc rescue nil }
84
+ RECEIVER_BODIES[Date] = %q{ v.nil? ? nil : Date.parse(v.to_s) rescue nil }
85
+ RECEIVER_BODIES[Array] = %q{ case when v.nil? then nil when v.blank? then [] else Array(v) end }
86
+ RECEIVER_BODIES[Hash] = %q{ case when v.nil? then nil when v.blank? then {} else v end }
87
+ RECEIVER_BODIES[Boolean] = %q{ case when v.nil? then nil when v.to_s.strip.blank? then false else v.to_s.strip != "false" end }
88
+ RECEIVER_BODIES[NilClass] = %q{ raise ArgumentError, "This field must be nil, but {#{v}} was given" unless (v.nil?) ; nil }
89
+ RECEIVER_BODIES[Object] = %q{ v } # accept and love the object just as it is
90
+
91
+ #
92
+ # Give each base class a receive method
93
+ #
94
+ RECEIVER_BODIES.each do |k,b|
95
+ if k.is_a?(Class)
96
+ k.class_eval <<-STR, __FILE__, __LINE__ + 1
97
+ def self.receive(v)
98
+ #{b}
99
+ end
100
+ STR
101
+ end
102
+ end
103
+
104
+ TYPE_ALIASES = {
105
+ :null => NilClass,
106
+ :boolean => Boolean,
107
+ :string => String, :bytes => String,
108
+ :symbol => Symbol,
109
+ :int => Integer, :integer => Integer, :long => Integer,
110
+ :time => Time, :date => Date,
111
+ :float => Float, :double => Float,
112
+ :hash => Hash, :map => Hash,
113
+ :array => Array,
114
+ } unless defined?(TYPE_ALIASES)
115
+
116
+ #
117
+ # modify object in place with new typecast values.
118
+ #
119
+ def receive! hsh={}
120
+ raise ArgumentError, "Can't receive (it isn't hashlike): {#{hsh.inspect}}" unless hsh.respond_to?(:[]) && hsh.respond_to?(:has_key?)
121
+ self.class.receiver_attr_names.each do |attr|
122
+ if hsh.has_key?(attr.to_sym) then val = hsh[attr.to_sym]
123
+ elsif hsh.has_key?(attr.to_s) then val = hsh[attr.to_s]
124
+ else next ; end
125
+ _receive_attr attr, val
126
+ end
127
+ impose_defaults!(hsh)
128
+ run_after_receivers(hsh)
129
+ self
130
+ end
131
+
132
+ # true if the attr is a receiver variable and it has been set
133
+ def attr_set?(attr)
134
+ receiver_attrs.has_key?(attr) && self.instance_variable_defined?("@#{attr}")
135
+ end
136
+
137
+ protected
138
+
139
+ def unset!(attr)
140
+ self.send(:remove_instance_variable, "@#{attr}") if self.instance_variable_defined?("@#{attr}")
141
+ end
142
+
143
+ def _receive_attr attr, val
144
+ self.send("receive_#{attr}", val)
145
+ end
146
+
147
+ def impose_defaults!(hsh)
148
+ self.class.receiver_defaults.each do |attr, val|
149
+ next if attr_set?(attr)
150
+ self.instance_variable_set "@#{attr}", val
151
+ end
152
+ end
153
+
154
+ def run_after_receivers(hsh)
155
+ self.class.after_receivers.each do |after_receiver|
156
+ self.instance_exec(hsh, &after_receiver)
157
+ end
158
+ end
159
+
160
+ public
161
+
162
+ module ClassMethods
163
+
164
+ #
165
+ # Returns a new instance with the given hash used to set all rcvrs.
166
+ #
167
+ # All args after the first are passed to the initializer.
168
+ #
169
+ # @param hsh [Hash] attr-value pairs to set on the newly created object
170
+ # @param *args [Array] arguments to pass to the constructor
171
+ # @return [Object] a new instance
172
+ def receive *args
173
+ hsh = args.extract_options!
174
+ raise ArgumentError, "Can't receive (it isn't hashlike): {#{hsh.inspect}} -- the hsh should be the *last* arg" unless hsh.respond_to?(:[]) && hsh.respond_to?(:has_key?)
175
+ obj = self.new(*args)
176
+ obj.receive!(hsh)
177
+ end
178
+
179
+ #
180
+ # define a receiver attribute.
181
+ # automatically generates an attr_accessor on the class if none exists
182
+ #
183
+ # @option [Boolean] :required - Adds an error on validation if the attribute is never set
184
+ # @option [Object] :default - After any receive! operation, attribute is set to this value unless attr_set? is true
185
+ # @option [Class] :of - For collections (Array, Hash, etc), the type of the collection's items
186
+ #
187
+ def rcvr name, type, info={}
188
+ name = name.to_sym
189
+ type = type_to_klass(type)
190
+ class_eval <<-STR, __FILE__, __LINE__ + 1
191
+ def receive_#{name}(v)
192
+ v = (#{receiver_body_for(type, info)}) ;
193
+ self.instance_variable_set("@#{name}", v)
194
+ end
195
+ STR
196
+ # careful here: don't modify parent's class_attribute in-place
197
+ self.receiver_attrs = self.receiver_attrs.dup
198
+ self.receiver_attr_names += [name] unless receiver_attr_names.include?(name)
199
+ self.receiver_attrs[name] = info.merge({ :name => name, :type => type })
200
+ end
201
+
202
+ # make a block to run after each time .receive! is invoked
203
+ def after_receive &block
204
+ self.after_receivers += [block]
205
+ end
206
+
207
+ # defines a receiver attribute, an attr_reader and an attr_writer
208
+ # attr_reader is skipped if the getter method is already defined;
209
+ # attr_writer is skipped if the setter method is already defined;
210
+ def rcvr_accessor name, type, info={}
211
+ attr_reader(name) unless method_defined?(name)
212
+ attr_writer(name) unless method_defined?("#{name}=")
213
+ rcvr name, type, info
214
+ end
215
+ # defines a receiver attribute and an attr_reader
216
+ # attr_reader is skipped if the getter method is already defined.
217
+ def rcvr_reader name, type, info={}
218
+ attr_reader(name) unless method_defined?(name)
219
+ rcvr name, type, info
220
+ end
221
+ # defines a receiver attribute and an attr_writer
222
+ # attr_writer is skipped if the setter method is already defined.
223
+ def rcvr_writer name, type, info={}
224
+ attr_writer(name) unless method_defined?("#{name}=")
225
+ rcvr name, type, info
226
+ end
227
+
228
+ #
229
+ # Defines a receiver for attributes sent to receive! that are
230
+ # * not defined as receivers
231
+ # * attribute name does not start with '_'
232
+ #
233
+ # @example
234
+ # class Foo ; include Receiver
235
+ # rcvr_accessor :bob, String
236
+ # rcvr_remaining :other_params
237
+ # end
238
+ # foo_obj = Foo.receive(:bob => 'hi, bob", :joe => 'hi, joe')
239
+ # # => <Foo @bob='hi, bob' @other_params={ :joe => 'hi, joe' }>
240
+ def rcvr_remaining name, info={}
241
+ rcvr_reader name, Hash, info
242
+ after_receive do |hsh|
243
+ remaining_vals_hsh = hsh.reject{|k,v| (receiver_attrs.include?(k)) || (k.to_s =~ /^_/) }
244
+ self._receive_attr name, remaining_vals_hsh
245
+ end
246
+ end
247
+
248
+ # a hash from attribute names to their default values if given
249
+ def receiver_defaults
250
+ defs = {}
251
+ receiver_attrs.each do |name, info|
252
+ defs[name] = info[:default] if info.has_key?(:default)
253
+ end
254
+ defs
255
+ end
256
+
257
+ protected
258
+ def receiver_body_for type, info
259
+ type = type_to_klass(type)
260
+ # Note that Array and Hash only need (and only get) special treatment when
261
+ # they have an :of => SomeType option.
262
+ case
263
+ when info[:of] && (type == Array)
264
+ %Q{ v.nil? ? nil : v.map{|el| #{info[:of]}.receive(el) } }
265
+ when info[:of] && (type == Hash)
266
+ %Q{ v.nil? ? nil : v.inject({}){|h, (el,val)| h[el] = #{info[:of]}.receive(val); h } }
267
+ when Receiver::RECEIVER_BODIES.include?(type)
268
+ Receiver::RECEIVER_BODIES[type]
269
+ when type.is_a?(Class)
270
+ %Q{v.blank? ? nil : #{type}.receive(v) }
271
+ # when (type.is_a?(Symbol) && type.to_s =~ /^[A-Z]/)
272
+ # # a hack so you can use a class not defined yet
273
+ # %Q{v.blank? ? nil : #{type}.receive(v) }
274
+ else
275
+ raise("Can't receive #{type} #{info}")
276
+ end
277
+ end
278
+
279
+ def type_to_klass(type)
280
+ case
281
+ when type.is_a?(Class) then return type
282
+ when TYPE_ALIASES.has_key?(type) then TYPE_ALIASES[type]
283
+ # when (type.is_a?(Symbol) && type.to_s =~ /^[A-Z]/) then type.to_s.constantize
284
+ else raise ArgumentError, "Can\'t handle type #{type}: is it a Class or one of the TYPE_ALIASES?"
285
+ end
286
+ end
287
+ end
288
+
289
+ module ClassMethods
290
+ # By default, the hashlike methods iterate over the receiver attributes.
291
+ # If you want to filter our add to the keys list, override this method
292
+ #
293
+ # @example
294
+ # def self.members
295
+ # super + [:firstname, :lastname] - [:fullname]
296
+ # end
297
+ #
298
+ def members
299
+ receiver_attr_names
300
+ end
301
+ end
302
+
303
+ # set up receiver attributes, and bring in methods from the ClassMethods module at class-level
304
+ def self.included base
305
+ base.class_eval do
306
+ class_attribute :receiver_attrs
307
+ class_attribute :receiver_attr_names
308
+ class_attribute :after_receivers
309
+ self.receiver_attrs = {} # info about the attr
310
+ self.receiver_attr_names = [] # ordered set of attr names
311
+ self.after_receivers = [] # blocks to execute following receive!
312
+ extend ClassMethods
313
+ end
314
+ end
315
+ end
@@ -0,0 +1,19 @@
1
+ require 'active_model'
2
+
3
+ module Receiver
4
+ class ActiveModelShim
5
+ extend ActiveModel::Naming
6
+
7
+ def to_model
8
+ self
9
+ end
10
+
11
+ def valid?() true end
12
+ def new_record?() true end
13
+ def destroyed?() false end
14
+
15
+ def errors
16
+ @_errors ||= ActiveModel::Errors.new(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,191 @@
1
+ module Receiver
2
+ #
3
+ # Makes a Receiver thingie behave mostly like a hash.
4
+ #
5
+ # By default, the hashlike methods iterate over the receiver attributes:
6
+ # instance #keys delegates to self.class.keys which calls
7
+ # receiver_attr_names. If you want to filter our add to the keys list, you
8
+ # can just override the class-level keys method (and call super, or not):
9
+ #
10
+ # def self.keys
11
+ # super + [:firstname, :lastname] - [:fullname]
12
+ # end
13
+ #
14
+ # All methods are defined naturally on [], []= and has_key? -- if you enjoy
15
+ #
16
+ #
17
+ # in addition to the below, by including Enumerable, this also adds
18
+ #
19
+ # :each_cons, :each_entry, :each_slice, :each_with_index, :each_with_object,
20
+ # :map, :collect, :collect_concat, :entries, :to_a, :flat_map, :inject, :reduce,
21
+ # :group_by, :chunk, :cycle, :partition, :reverse_each, :slice_before, :drop,
22
+ # :drop_while, :take, :take_while, :detect, :find, :find_all, :find_index, :grep,
23
+ # :all?, :any?, :none?, :one?, :first, :count, :zip :max, :max_by, :min, :min_by,
24
+ # :minmax, :minmax_by, :sort, :sort_by
25
+ #
26
+ # As opposed to hash, does *not* define
27
+ #
28
+ # default, default=, default_proc, default_proc=, shift, flatten, compare_by_identity
29
+ # compare_by_identity? rehash
30
+ #
31
+ module ActsAsHash
32
+
33
+ # Hashlike#[]
34
+ #
35
+ # Element Reference -- Retrieves the value stored for +key+.
36
+ #
37
+ # In a normal hash, a default value can be set; none is provided here.
38
+ #
39
+ # Delegates to self.send(key)
40
+ #
41
+ # @example
42
+ # hsh = { :a => 100, :b => 200 }
43
+ # hsh[:a] # => 100
44
+ # hsh[:c] # => nil
45
+ #
46
+ # @param key [Object] key to retrieve
47
+ # @return [Object] the value stored for key, nil if missing
48
+ #
49
+ def [](key)
50
+ key = convert_key(key)
51
+ self.send(key)
52
+ end
53
+
54
+ # Hashlike#[]=
55
+ # Hashlike#store
56
+ #
57
+ # Element Assignment -- Associates the value given by +val+ with the key
58
+ # given by +key+.
59
+ #
60
+ # key should not have its value changed while it is in use as a key. In a
61
+ # normal hash, a String passed as a key will be duplicated and frozen. No such
62
+ # guarantee is provided here
63
+ #
64
+ # Delegates to self.send("key=", val)
65
+ #
66
+ # @example
67
+ # hsh = { :a => 100, :b => 200 }
68
+ # hsh[:a] = 9
69
+ # hsh[:c] = 4
70
+ # hsh # => { :a => 9, :b => 200, :c => 4 }
71
+ #
72
+ # hsh[key] = val -> val
73
+ # hsh.store(key, val) -> val
74
+ #
75
+ # @param key [Object] key to associate
76
+ # @param val [Object] value to associate it with
77
+ # @return [Object]
78
+ #
79
+ def []=(key, val)
80
+ key = convert_key(key)
81
+ self.send("#{key}=", val)
82
+ end
83
+
84
+ # Hashlike#delete
85
+ #
86
+ # Deletes and returns the value from +hsh+ whose key is equal to +key+. If the
87
+ # optional code block is given and the key is not found, pass in the key and
88
+ # return the result of +block+.
89
+ #
90
+ # In a normal hash, a default value can be set; none is provided here.
91
+ #
92
+ # @example
93
+ # hsh = { :a => 100, :b => 200 }
94
+ # hsh.delete(:a) # => 100
95
+ # hsh.delete(:z) # => nil
96
+ # hsh.delete(:z){|el| "#{el} not found" } # => "z not found"
97
+ #
98
+ # @overload hsh.delete(key) -> val
99
+ # @param key [Object] key to remove
100
+ # @return [Object, Nil] the removed object, nil if missing
101
+ #
102
+ # @overload hsh.delete(key){|key| block } -> val
103
+ # @param key [Object] key to remove
104
+ # @yield [Object] called (with key) if key is missing
105
+ # @yieldparam key
106
+ # @return [Object, Nil] the removed object, or if missing, the return value
107
+ # of the block
108
+ #
109
+ def delete(key, &block)
110
+ key = convert_key(key)
111
+ if has_key?(key)
112
+ val = self[key]
113
+ self.send(:remove_instance_variable, "@#{key}")
114
+ val
115
+ elsif block_given?
116
+ block.call(key)
117
+ else
118
+ nil
119
+ end
120
+ end
121
+
122
+ # # Hashlike#==
123
+ # #
124
+ # # Equality -- Two hashes are equal if they contain the same number of keys,
125
+ # # and the value corresponding to each key in the first hash is equal (using
126
+ # # <tt>==</tt>) to the value for the same key in the second. If +obj+ is not a
127
+ # # Hashlike, attempt to convert it using +to_hash+ and return <tt>obj ==
128
+ # # hsh</tt>.
129
+ # #
130
+ # # Does not take a default value comparion into account.
131
+ # #
132
+ # # @example
133
+ # # h1 = { :a => 1, :c => 2 }
134
+ # # h2 = { 7 => 35, :c => 2, :a => 1 }
135
+ # # h3 = { :a => 1, :c => 2, 7 => 35 }
136
+ # # h4 = { :a => 1, :d => 2, :f => 35 }
137
+ # # h1 == h2 # => false
138
+ # # h2 == h3 # => true
139
+ # # h3 == h4 # => false
140
+ # #
141
+ # def ==(other_hash)
142
+ # (length == other_hash.length) &&
143
+ # all?{|k,v| v == other_hash[k] }
144
+ # end
145
+
146
+ # Hashlike#keys
147
+ #
148
+ # Returns a new array populated with the keys from this hashlike.
149
+ #
150
+ # @see Hashlike#values.
151
+ #
152
+ # @example
153
+ # hsh = { :a => 100, :b => 200, :c => 300, :d => 400 }
154
+ # hsh.keys # => [:a, :b, :c, :d]
155
+ #
156
+ # @return [Array] list of keys
157
+ #
158
+ def keys
159
+ members & instance_variables.map{|s| convert_key(s[1..-1]) }
160
+ end
161
+
162
+ def members
163
+ self.class.members
164
+ end
165
+
166
+ module ClassMethods
167
+ # By default, the hashlike methods iterate over the receiver attributes.
168
+ # If you want to filter our add to the keys list, override this method
169
+ #
170
+ # @example
171
+ # def self.keys
172
+ # super + [:firstname, :lastname] - [:fullname]
173
+ # end
174
+ #
175
+ def keys
176
+ receiver_attr_names
177
+ end
178
+ end
179
+
180
+ protected
181
+
182
+ def convert_key(key)
183
+ raise ArgumentError, "Keys for #{self.class} must be symbols, strings or respond to #to_sym" unless key.respond_to?(:to_sym)
184
+ key.to_sym
185
+ end
186
+
187
+ def self.included base
188
+ base.extend ClassMethods
189
+ end
190
+ end
191
+ end