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.
- data/CHANGELOG.textile +6 -0
- data/README.textile +34 -11
- data/VERSION +1 -1
- data/gorillib.gemspec +63 -5
- data/lib/gorillib/enumerable/sum.rb +2 -2
- data/lib/gorillib/hash/compact.rb +2 -29
- data/lib/gorillib/hash/deep_compact.rb +2 -12
- data/lib/gorillib/hash/deep_dup.rb +4 -0
- data/lib/gorillib/hash/deep_merge.rb +2 -14
- data/lib/gorillib/hash/indifferent_access.rb +207 -0
- data/lib/gorillib/hash/keys.rb +2 -40
- data/lib/gorillib/hash/reverse_merge.rb +2 -24
- data/lib/gorillib/hash/slice.rb +2 -51
- data/lib/gorillib/hash/tree_merge.rb +4 -0
- data/lib/gorillib/hashlike.rb +824 -0
- data/lib/gorillib/hashlike/compact.rb +60 -0
- data/lib/gorillib/hashlike/deep_compact.rb +18 -0
- data/lib/gorillib/hashlike/deep_dup.rb +15 -0
- data/lib/gorillib/hashlike/deep_merge.rb +20 -0
- data/lib/gorillib/hashlike/hashlike_via_accessors.rb +169 -0
- data/lib/gorillib/hashlike/keys.rb +59 -0
- data/lib/gorillib/hashlike/reverse_merge.rb +31 -0
- data/lib/gorillib/hashlike/slice.rb +67 -0
- data/lib/gorillib/hashlike/tree_merge.rb +76 -0
- data/lib/gorillib/metaprogramming/mattr_accessor.rb +1 -1
- data/lib/gorillib/receiver.rb +315 -0
- data/lib/gorillib/receiver/active_model_shim.rb +19 -0
- data/lib/gorillib/receiver/acts_as_hash.rb +191 -0
- data/lib/gorillib/receiver/acts_as_loadable.rb +42 -0
- data/lib/gorillib/receiver/tree_diff.rb +74 -0
- data/lib/gorillib/receiver/validations.rb +30 -0
- data/lib/gorillib/struct/acts_as_hash.rb +108 -0
- data/lib/gorillib/struct/hashlike_iteration.rb +0 -0
- data/notes/fancy_hashes_and_receivers.textile +120 -0
- data/notes/hash_rdocs.textile +97 -0
- data/spec/hash/deep_merge_spec.rb +0 -2
- data/spec/hash/indifferent_access_spec.rb +391 -0
- data/spec/hash/slice_spec.rb +35 -12
- data/spec/hashlike/behave_same_as_hash_spec.rb +105 -0
- data/spec/hashlike/hashlike_behavior_spec.rb +824 -0
- data/spec/hashlike/hashlike_via_accessors_fuzzing_spec.rb +37 -0
- data/spec/hashlike/hashlike_via_accessors_spec.rb +262 -0
- data/spec/hashlike_spec.rb +302 -0
- data/spec/metaprogramming/aliasing_spec.rb +3 -0
- data/spec/metaprogramming/cattr_accessor_spec.rb +2 -0
- data/spec/metaprogramming/class_attribute_spec.rb +2 -0
- data/spec/metaprogramming/delegation_spec.rb +2 -0
- data/spec/metaprogramming/mattr_accessor_spec.rb +2 -0
- data/spec/metaprogramming/singleton_class_spec.rb +3 -0
- data/spec/receiver/acts_as_hash_spec.rb +286 -0
- data/spec/receiver_spec.rb +478 -0
- data/spec/spec_helper.rb +11 -6
- data/spec/string/truncate_spec.rb +1 -0
- data/spec/struct/acts_as_hash_fuzz_spec.rb +67 -0
- data/spec/struct/acts_as_hash_spec.rb +426 -0
- data/spec/support/hashlike_fuzzing_helper.rb +127 -0
- data/spec/support/hashlike_helper.rb +75 -0
- data/spec/support/hashlike_struct_helper.rb +37 -0
- data/spec/support/hashlike_via_delegation.rb +30 -0
- data/spec/support/matchers/be_array_eql.rb +12 -0
- data/spec/support/matchers/be_hash_eql.rb +14 -0
- data/spec/support/matchers/enumerate_method.rb +10 -0
- data/spec/support/matchers/evaluate_to_true.rb +5 -0
- 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
|
@@ -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
|