gorillib 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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