gorillib 0.5.0 → 0.5.2
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/.gitignore +3 -0
- data/.gitmodules +3 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -16
- data/Rakefile +2 -74
- data/away/aliasing_spec.rb +180 -0
- data/away/confidence.rb +17 -0
- data/away/stub_module.rb +33 -0
- data/gorillib.gemspec +31 -246
- data/lib/gorillib/collection/model_collection.rb +1 -0
- data/lib/gorillib/data_munging.rb +0 -1
- data/lib/gorillib/hashlike/slice.rb +2 -0
- data/lib/gorillib/model/field.rb +2 -2
- data/lib/gorillib/model/serialization.rb +9 -4
- data/lib/gorillib/model/serialization/csv.rb +1 -0
- data/lib/gorillib/model/serialization/lines.rb +2 -0
- data/lib/gorillib/model/serialization/tsv.rb +1 -0
- data/lib/gorillib/pathname.rb +1 -1
- data/lib/gorillib/pathname/utils.rb +6 -0
- data/lib/gorillib/string/inflector.rb +1 -1
- data/lib/gorillib/system.rb +1 -0
- data/lib/gorillib/system/runner.rb +36 -0
- data/lib/gorillib/type/ip_address.rb +2 -2
- data/lib/gorillib/version.rb +3 -0
- data/old/lib/gorillib/hash/indifferent_access.rb +207 -0
- data/old/lib/gorillib/hash/tree_merge.rb +4 -0
- data/old/lib/gorillib/hashlike/tree_merge.rb +49 -0
- data/old/lib/gorillib/metaprogramming/cattr_accessor.rb +79 -0
- data/old/lib/gorillib/metaprogramming/mattr_accessor.rb +61 -0
- data/old/lib/gorillib/receiver.rb +402 -0
- data/old/lib/gorillib/receiver/active_model_shim.rb +32 -0
- data/old/lib/gorillib/receiver/acts_as_hash.rb +195 -0
- data/old/lib/gorillib/receiver/acts_as_loadable.rb +42 -0
- data/old/lib/gorillib/receiver/locale/en.yml +27 -0
- data/old/lib/gorillib/receiver/tree_diff.rb +74 -0
- data/old/lib/gorillib/receiver/validations.rb +30 -0
- data/old/lib/gorillib/receiver_model.rb +21 -0
- data/old/lib/gorillib/struct/acts_as_hash.rb +108 -0
- data/old/lib/gorillib/struct/hashlike_iteration.rb +0 -0
- data/old/spec/gorillib/hash/indifferent_access_spec.rb +391 -0
- data/old/spec/gorillib/metaprogramming/cattr_accessor_spec.rb +43 -0
- data/old/spec/gorillib/metaprogramming/mattr_accessor_spec.rb +45 -0
- data/old/spec/gorillib/receiver/receiver/acts_as_hash_spec.rb +295 -0
- data/old/spec/gorillib/receiver_spec.rb +551 -0
- data/old/spec/gorillib/struct/acts_as_hash_fuzz_spec.rb +71 -0
- data/old/spec/gorillib/struct/acts_as_hash_spec.rb +422 -0
- data/spec/gorillib/array/compact_blank_spec.rb +2 -2
- data/spec/gorillib/collection_spec.rb +6 -6
- data/spec/gorillib/factories_spec.rb +2 -2
- data/spec/gorillib/hashlike_spec.rb +2 -1
- data/spec/gorillib/model/defaults_spec.rb +3 -3
- data/spec/gorillib/model/serialization/csv_spec.rb +35 -0
- data/spec/gorillib/model/serialization/tsv_spec.rb +20 -4
- data/spec/gorillib/model/serialization_spec.rb +3 -3
- data/spec/spec_helper.rb +6 -1
- data/spec/support/factory_test_helpers.rb +2 -2
- data/spec/support/gorillib_test_helpers.rb +4 -4
- data/spec/support/hashlike_fuzzing_helper.rb +1 -15
- data/spec/support/hashlike_helper.rb +5 -1
- data/spec/support/model_test_helpers.rb +12 -1
- metadata +192 -168
- data/notes/HOWTO.md +0 -22
- data/notes/bucket.md +0 -155
- data/notes/builder.md +0 -170
- data/notes/collection.md +0 -81
- data/notes/factories.md +0 -86
- data/notes/model-overlay.md +0 -209
- data/notes/model.md +0 -135
- data/notes/structured-data-classes.md +0 -127
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'gorillib/array/extract_options'
|
2
|
+
|
3
|
+
# Extends the class object with class and instance accessors for class attributes,
|
4
|
+
# just like the native attr* accessors for instance attributes.
|
5
|
+
#
|
6
|
+
# Note that unlike +class_attribute+, if a subclass changes the value then that would
|
7
|
+
# also change the value for parent class. Similarly if parent class changes the value
|
8
|
+
# then that would change the value of subclasses too.
|
9
|
+
#
|
10
|
+
# class Person
|
11
|
+
# cattr_accessor :hair_colors
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
15
|
+
# Person.hair_colors # => [:brown, :black, :blonde, :red]
|
16
|
+
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
|
17
|
+
#
|
18
|
+
# To opt out of the instance writer method, pass :instance_writer => false.
|
19
|
+
# To opt out of the instance reader method, pass :instance_reader => false.
|
20
|
+
#
|
21
|
+
# class Person
|
22
|
+
# cattr_accessor :hair_colors, :instance_writer => false, :instance_reader => false
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# Person.new.hair_colors = [:brown] # => NoMethodError
|
26
|
+
# Person.new.hair_colors # => NoMethodError
|
27
|
+
class Class
|
28
|
+
def cattr_reader(*syms)
|
29
|
+
options = syms.extract_options!
|
30
|
+
syms.each do |sym|
|
31
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
32
|
+
unless defined? @@#{sym}
|
33
|
+
@@#{sym} = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.#{sym}
|
37
|
+
@@#{sym}
|
38
|
+
end
|
39
|
+
EOS
|
40
|
+
|
41
|
+
unless options[:instance_reader] == false
|
42
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
43
|
+
def #{sym}
|
44
|
+
@@#{sym}
|
45
|
+
end
|
46
|
+
EOS
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end unless method_defined?(:cattr_reader)
|
50
|
+
|
51
|
+
def cattr_writer(*syms)
|
52
|
+
options = syms.extract_options!
|
53
|
+
syms.each do |sym|
|
54
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
55
|
+
unless defined? @@#{sym}
|
56
|
+
@@#{sym} = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.#{sym}=(obj)
|
60
|
+
@@#{sym} = obj
|
61
|
+
end
|
62
|
+
EOS
|
63
|
+
|
64
|
+
unless options[:instance_writer] == false
|
65
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
66
|
+
def #{sym}=(obj)
|
67
|
+
@@#{sym} = obj
|
68
|
+
end
|
69
|
+
EOS
|
70
|
+
end
|
71
|
+
self.send("#{sym}=", yield) if block_given?
|
72
|
+
end
|
73
|
+
end unless method_defined?(:cattr_writer)
|
74
|
+
|
75
|
+
def cattr_accessor(*syms, &blk)
|
76
|
+
cattr_reader(*syms)
|
77
|
+
cattr_writer(*syms, &blk)
|
78
|
+
end unless method_defined?(:cattr_accessor)
|
79
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'gorillib/array/extract_options'
|
2
|
+
|
3
|
+
class Module
|
4
|
+
def mattr_reader(*syms)
|
5
|
+
options = syms.extract_options!
|
6
|
+
syms.each do |sym|
|
7
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
8
|
+
@@#{sym} = nil unless defined? @@#{sym}
|
9
|
+
|
10
|
+
def self.#{sym}
|
11
|
+
@@#{sym}
|
12
|
+
end
|
13
|
+
EOS
|
14
|
+
|
15
|
+
unless options[:instance_reader] == false
|
16
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
17
|
+
def #{sym}
|
18
|
+
@@#{sym}
|
19
|
+
end
|
20
|
+
EOS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end unless method_defined?(:mattr_reader)
|
24
|
+
|
25
|
+
def mattr_writer(*syms)
|
26
|
+
options = syms.extract_options!
|
27
|
+
syms.each do |sym|
|
28
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
29
|
+
def self.#{sym}=(obj)
|
30
|
+
@@#{sym} = obj
|
31
|
+
end
|
32
|
+
EOS
|
33
|
+
|
34
|
+
unless options[:instance_writer] == false
|
35
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
36
|
+
def #{sym}=(obj)
|
37
|
+
@@#{sym} = obj
|
38
|
+
end
|
39
|
+
EOS
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end unless method_defined?(:mattr_writer)
|
43
|
+
|
44
|
+
# Extends the module object with module and instance accessors for class attributes,
|
45
|
+
# just like the native attr* accessors for instance attributes.
|
46
|
+
#
|
47
|
+
# module AppConfiguration
|
48
|
+
# mattr_accessor :google_api_key
|
49
|
+
# self.google_api_key = "123456789"
|
50
|
+
#
|
51
|
+
# mattr_accessor :paypal_url
|
52
|
+
# self.paypal_url = "www.sandbox.paypal.com"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# AppConfiguration.google_api_key = "overriding the api key!"
|
56
|
+
def mattr_accessor(*syms)
|
57
|
+
mattr_reader(*syms)
|
58
|
+
mattr_writer(*syms)
|
59
|
+
end unless method_defined?(:mattr_accessor)
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,402 @@
|
|
1
|
+
require 'gorillib/object/blank'
|
2
|
+
require 'gorillib/object/try'
|
3
|
+
require 'gorillib/object/try_dup'
|
4
|
+
require 'gorillib/array/extract_options'
|
5
|
+
require 'gorillib/metaprogramming/class_attribute'
|
6
|
+
|
7
|
+
# dummy type for receiving True or False
|
8
|
+
class Boolean ; end unless defined?(Boolean)
|
9
|
+
|
10
|
+
# Receiver lets you describe complex (even recursive!) actively-typed data models that
|
11
|
+
# * are creatable or assignable from static data structures
|
12
|
+
# * perform efficient type conversion when assigning from a data structure,
|
13
|
+
# * but with nothing in the way of normal assignment or instantiation
|
14
|
+
# * and no requirements on the initializer
|
15
|
+
#
|
16
|
+
# class Tweet
|
17
|
+
# include Receiver
|
18
|
+
# rcvr_accessor :id, Integer
|
19
|
+
# rcvr_accessor :user_id, Integer
|
20
|
+
# rcvr_accessor :created_at, Time
|
21
|
+
# end
|
22
|
+
# p Tweet.receive(:id => "7", :user_id => 9, :created_at => "20101231010203" )
|
23
|
+
# # => #<Tweet @id=7, @user_id=9, @created_at=2010-12-31 07:02:03 UTC>
|
24
|
+
#
|
25
|
+
# You can override receive behavior in a straightforward and predictable way:
|
26
|
+
#
|
27
|
+
# class TwitterUser
|
28
|
+
# include Receiver
|
29
|
+
# rcvr_accessor :id, Integer
|
30
|
+
# rcvr_accessor :screen_name, String
|
31
|
+
# rcvr_accessor :follower_ids, Array, :of => Integer
|
32
|
+
# # accumulate unique follower ids
|
33
|
+
# def receive_follower_ids(arr)
|
34
|
+
# @follower_ids = (@follower_ids||[]) + arr.map(&:to_i)
|
35
|
+
# @follower_ids.uniq!
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# The receiver pattern works naturally with inheritance:
|
40
|
+
#
|
41
|
+
# class TweetWithUser < Tweet
|
42
|
+
# rcvr_accessor :user, TwitterUser
|
43
|
+
# after_receive do |hsh|
|
44
|
+
# self.user_id = self.user.id if self.user
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# p TweetWithUser.receive(:id => 8675309, :created_at => "20101231010203", :user => { :id => 24601, :screen_name => 'bob', :follower_ids => [1, 8, 3, 4] })
|
48
|
+
# => #<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>
|
49
|
+
#
|
50
|
+
# TweetWithUser was able to add another receiver, applicable only to itself and its subclasses.
|
51
|
+
#
|
52
|
+
# The receive method works well with sparse data -- you can accumulate
|
53
|
+
# attributes without trampling formerly set values:
|
54
|
+
#
|
55
|
+
# tw = Tweet.receive(:id => "7", :user_id => 9 )
|
56
|
+
# p tw
|
57
|
+
# # => #<Tweet @id=7, @user_id=9>
|
58
|
+
#
|
59
|
+
# tw.receive!(:created_at => "20101231010203" )
|
60
|
+
# p tw
|
61
|
+
# # => #<Tweet @id=7, @user_id=9, @created_at=2010-12-31 07:02:03 UTC>
|
62
|
+
#
|
63
|
+
# Note the distinction between an explicit nil field and a missing field:
|
64
|
+
#
|
65
|
+
# tw.receive!(:user_id => nil, :created_at => "20090506070809" )
|
66
|
+
# p tw
|
67
|
+
# # => #<Tweet @id=7, @user_id=nil, @created_at=2009-05-06 12:08:09 UTC>
|
68
|
+
#
|
69
|
+
# There are helpers for default and required attributes:
|
70
|
+
#
|
71
|
+
# class Foo
|
72
|
+
# include Receiver
|
73
|
+
# rcvr_accessor :is_reqd, String, :required => true
|
74
|
+
# rcvr_accessor :also_reqd, String, :required => true
|
75
|
+
# rcvr_accessor :has_default, String, :default => 'hello'
|
76
|
+
# end
|
77
|
+
# foo_obj = Foo.receive(:is_reqd => "hi")
|
78
|
+
# # => #<Foo:0x00000100bd9740 @is_reqd="hi" @has_default="hello">
|
79
|
+
# foo_obj.missing_attrs
|
80
|
+
# # => [:also_reqd]
|
81
|
+
#
|
82
|
+
module Receiver
|
83
|
+
|
84
|
+
RECEIVER_BODIES = {} unless defined?(RECEIVER_BODIES)
|
85
|
+
RECEIVER_BODIES[Symbol] = %q{ v.blank? ? nil : v.to_sym }
|
86
|
+
RECEIVER_BODIES[Integer] = %q{ v.blank? ? nil : v.to_i }
|
87
|
+
RECEIVER_BODIES[Float] = %q{ v.blank? ? nil : v.to_f }
|
88
|
+
RECEIVER_BODIES[String] = %q{ v.to_s }
|
89
|
+
RECEIVER_BODIES[Time] = %q{ v.nil? ? nil : Time.parse(v.to_s).utc rescue nil }
|
90
|
+
RECEIVER_BODIES[Date] = %q{ v.nil? ? nil : Date.parse(v.to_s) rescue nil }
|
91
|
+
RECEIVER_BODIES[Array] = %q{ case when v.nil? then nil when v.blank? then [] else Array(v) end }
|
92
|
+
RECEIVER_BODIES[Hash] = %q{ case when v.nil? then nil when v.blank? then {} else v end }
|
93
|
+
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 }
|
94
|
+
RECEIVER_BODIES[NilClass] = %q{ raise ArgumentError, "This field must be nil, but [#{v}] was given" unless (v.nil?) ; nil }
|
95
|
+
RECEIVER_BODIES[Object] = %q{ v } # accept and love the object just as it is
|
96
|
+
|
97
|
+
#
|
98
|
+
# Give each base class a receive method
|
99
|
+
#
|
100
|
+
RECEIVER_BODIES.each do |k,b|
|
101
|
+
if k.is_a?(Class) && b.is_a?(String)
|
102
|
+
k.class_eval <<-STR, __FILE__, __LINE__ + 1
|
103
|
+
def self.receive(v)
|
104
|
+
#{b}
|
105
|
+
end
|
106
|
+
STR
|
107
|
+
elsif k.is_a?(Class)
|
108
|
+
k.class_eval do
|
109
|
+
define_singleton_method(:receive, &b)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
TYPE_ALIASES = {
|
115
|
+
:null => NilClass,
|
116
|
+
:boolean => Boolean,
|
117
|
+
:string => String, :bytes => String,
|
118
|
+
:symbol => Symbol,
|
119
|
+
:int => Integer, :integer => Integer, :long => Integer,
|
120
|
+
:time => Time, :date => Date,
|
121
|
+
:float => Float, :double => Float,
|
122
|
+
:hash => Hash, :map => Hash,
|
123
|
+
:array => Array,
|
124
|
+
} unless defined?(TYPE_ALIASES)
|
125
|
+
|
126
|
+
#
|
127
|
+
# modify object in place with new typecast values.
|
128
|
+
#
|
129
|
+
def receive! hsh={}
|
130
|
+
raise ArgumentError, "Can't receive (it isn't hashlike): {#{hsh.inspect}}" unless hsh.respond_to?(:[]) && hsh.respond_to?(:has_key?)
|
131
|
+
_receiver_fields.each do |attr|
|
132
|
+
if hsh.has_key?(attr.to_sym) then val = hsh[attr.to_sym]
|
133
|
+
elsif hsh.has_key?(attr.to_s) then val = hsh[attr.to_s]
|
134
|
+
else next ; end
|
135
|
+
_receive_attr attr, val
|
136
|
+
end
|
137
|
+
impose_defaults!(hsh)
|
138
|
+
replace_options!(hsh)
|
139
|
+
run_after_receivers(hsh)
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
# true if the attr is a receiver variable and it has been set
|
144
|
+
def attr_set?(attr)
|
145
|
+
receiver_attrs.has_key?(attr) && self.instance_variable_defined?("@#{attr}")
|
146
|
+
end
|
147
|
+
|
148
|
+
protected
|
149
|
+
|
150
|
+
def unset!(attr)
|
151
|
+
self.send(:remove_instance_variable, "@#{attr}") if self.instance_variable_defined?("@#{attr}")
|
152
|
+
end
|
153
|
+
|
154
|
+
def _receive_attr attr, val
|
155
|
+
self.send("receive_#{attr}", val)
|
156
|
+
end
|
157
|
+
|
158
|
+
def _receiver_fields
|
159
|
+
self.class.receiver_attr_names
|
160
|
+
end
|
161
|
+
|
162
|
+
def _receiver_defaults
|
163
|
+
self.class.receiver_defaults
|
164
|
+
end
|
165
|
+
|
166
|
+
def _after_receivers
|
167
|
+
self.class.after_receivers
|
168
|
+
end
|
169
|
+
|
170
|
+
def impose_defaults!(hsh)
|
171
|
+
_receiver_defaults.each do |attr, val|
|
172
|
+
next if attr_set?(attr)
|
173
|
+
self.instance_variable_set "@#{attr}", val.try_dup
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# class Foo
|
178
|
+
# include Receiver
|
179
|
+
# include Receiver::ActsAsHash
|
180
|
+
# rcvr_accessor :attribute, String, :default => 'okay' :replace => { 'bad' => 'good' }
|
181
|
+
# end
|
182
|
+
#
|
183
|
+
# f = Foo.receive({:attribute => 'bad'})
|
184
|
+
# => #<Foo:0x10156c820 @attribute="good">
|
185
|
+
#
|
186
|
+
def replace_options!(hsh)
|
187
|
+
self.receiver_attrs.each do |attr, info|
|
188
|
+
val = self.instance_variable_get("@#{attr}")
|
189
|
+
if info[:replace] and info[:replace].has_key? val
|
190
|
+
self.instance_variable_set "@#{attr}", info[:replace][val]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def run_after_receivers(hsh)
|
196
|
+
_after_receivers.each do |after_receiver|
|
197
|
+
self.instance_exec(hsh, &after_receiver)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
public
|
202
|
+
|
203
|
+
module ClassMethods
|
204
|
+
|
205
|
+
#
|
206
|
+
# Returns a new instance with the given hash used to set all rcvrs.
|
207
|
+
#
|
208
|
+
# All args up to the last one are passed to the initializer.
|
209
|
+
# The last arg must be a hash -- its attributes are set on the newly-created object
|
210
|
+
#
|
211
|
+
# @param hsh [Hash] attr-value pairs to set on the newly created object.
|
212
|
+
# @param *args [Array] arguments to pass to the constructor
|
213
|
+
# @return [Object] a new instance
|
214
|
+
def receive *args
|
215
|
+
hsh = args.pop || {}
|
216
|
+
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?)
|
217
|
+
obj = self.new(*args)
|
218
|
+
obj.receive!(hsh)
|
219
|
+
end
|
220
|
+
|
221
|
+
#
|
222
|
+
# define a receiver attribute.
|
223
|
+
# automatically generates an attr_accessor on the class if none exists
|
224
|
+
#
|
225
|
+
# @option [Boolean] :required - Adds an error on validation if the attribute is never set
|
226
|
+
# @option [Object] :default - After any receive! operation, attribute is set to this value unless attr_set? is true
|
227
|
+
# @option [Class] :of - For collections (Array, Hash, etc), the type of the collection's items
|
228
|
+
#
|
229
|
+
def rcvr name, type, info={}
|
230
|
+
name = name.to_sym
|
231
|
+
type = type_to_klass(type)
|
232
|
+
body = receiver_body_for(type, info)
|
233
|
+
if body.is_a?(String)
|
234
|
+
class_eval(%Q{
|
235
|
+
def receive_#{name}(v)
|
236
|
+
self.instance_variable_set("@#{name}", (#{body}))
|
237
|
+
end}, __FILE__, __LINE__ + 1)
|
238
|
+
else
|
239
|
+
define_method("receive_#{name}") do |*args|
|
240
|
+
v = body.call(*args)
|
241
|
+
self.instance_variable_set("@#{name}", v)
|
242
|
+
v
|
243
|
+
end
|
244
|
+
end
|
245
|
+
# careful here: don't modify parent's class_attribute in-place
|
246
|
+
self.receiver_attrs = self.receiver_attrs.dup
|
247
|
+
self.receiver_attr_names += [name] unless receiver_attr_names.include?(name)
|
248
|
+
self.receiver_attrs[name] = info.merge({ :name => name, :type => type })
|
249
|
+
end
|
250
|
+
|
251
|
+
# make a block to run after each time .receive! is invoked
|
252
|
+
def after_receive &block
|
253
|
+
self.after_receivers += [block]
|
254
|
+
end
|
255
|
+
|
256
|
+
# defines a receiver attribute, an attr_reader and an attr_writer
|
257
|
+
# attr_reader is skipped if the getter method is already defined;
|
258
|
+
# attr_writer is skipped if the setter method is already defined;
|
259
|
+
def rcvr_accessor name, type, info={}
|
260
|
+
attr_reader(name) unless method_defined?(name)
|
261
|
+
attr_writer(name) unless method_defined?("#{name}=")
|
262
|
+
rcvr name, type, info
|
263
|
+
end
|
264
|
+
# defines a receiver attribute and an attr_reader
|
265
|
+
# attr_reader is skipped if the getter method is already defined.
|
266
|
+
def rcvr_reader name, type, info={}
|
267
|
+
attr_reader(name) unless method_defined?(name)
|
268
|
+
rcvr name, type, info
|
269
|
+
end
|
270
|
+
# defines a receiver attribute and an attr_writer
|
271
|
+
# attr_writer is skipped if the setter method is already defined.
|
272
|
+
def rcvr_writer name, type, info={}
|
273
|
+
attr_writer(name) unless method_defined?("#{name}=")
|
274
|
+
rcvr name, type, info
|
275
|
+
end
|
276
|
+
|
277
|
+
#
|
278
|
+
# Defines a receiver for attributes sent to receive! that are
|
279
|
+
# * not defined as receivers
|
280
|
+
# * attribute name does not start with '_'
|
281
|
+
#
|
282
|
+
# @example
|
283
|
+
# class Foo ; include Receiver
|
284
|
+
# rcvr_accessor :bob, String
|
285
|
+
# rcvr_remaining :other_params
|
286
|
+
# end
|
287
|
+
# foo_obj = Foo.receive(:bob => 'hi, bob", :joe => 'hi, joe')
|
288
|
+
# # => <Foo @bob='hi, bob' @other_params={ :joe => 'hi, joe' }>
|
289
|
+
def rcvr_remaining name, info={}
|
290
|
+
rcvr_reader name, Hash, info
|
291
|
+
after_receive do |hsh|
|
292
|
+
remaining_vals_hsh = hsh.reject{|k,v| (receiver_attrs.include?(k)) || (k.to_s =~ /^_/) }
|
293
|
+
self._receive_attr name, remaining_vals_hsh
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# a hash from attribute names to their default values if given
|
298
|
+
def receiver_defaults
|
299
|
+
defs = {}
|
300
|
+
receiver_attrs.each do |name, info|
|
301
|
+
defs[name] = info[:default] if info.has_key?(:default)
|
302
|
+
end
|
303
|
+
defs
|
304
|
+
end
|
305
|
+
|
306
|
+
# returns an in-order traversal of the
|
307
|
+
#
|
308
|
+
def tuple_keys
|
309
|
+
return @tuple_keys if @tuple_keys
|
310
|
+
@tuple_keys = self
|
311
|
+
@tuple_keys = receiver_attrs.map do |attr, info|
|
312
|
+
info[:type].try(:tuple_keys) || attr
|
313
|
+
end.flatten
|
314
|
+
end
|
315
|
+
|
316
|
+
def consume_tuple(tuple)
|
317
|
+
obj = self.new
|
318
|
+
receiver_attrs.each do |attr, info|
|
319
|
+
if info[:type].respond_to?(:consume_tuple)
|
320
|
+
val = info[:type].consume_tuple(tuple)
|
321
|
+
else
|
322
|
+
val = tuple.shift
|
323
|
+
end
|
324
|
+
# obj.send("receive_#{attr}", val)
|
325
|
+
obj.send("#{attr}=", val)
|
326
|
+
end
|
327
|
+
obj
|
328
|
+
end
|
329
|
+
|
330
|
+
protected
|
331
|
+
def receiver_body_for type, info
|
332
|
+
type = type_to_klass(type)
|
333
|
+
# Note that Array and Hash only need (and only get) special treatment when
|
334
|
+
# they have an :of => SomeType option.
|
335
|
+
case
|
336
|
+
when info[:of] && (type == Array)
|
337
|
+
receiver_type = info[:of]
|
338
|
+
lambda{|v| v.nil? ? nil : v.map{|el| receiver_type.receive(el) } }
|
339
|
+
when info[:of] && (type == Hash)
|
340
|
+
receiver_type = info[:of]
|
341
|
+
lambda{|v| v.nil? ? nil : v.inject({}){|h, (el,val)| h[el] = receiver_type.receive(val); h } }
|
342
|
+
when Receiver::RECEIVER_BODIES.include?(type)
|
343
|
+
Receiver::RECEIVER_BODIES[type]
|
344
|
+
when type.is_a?(Class)
|
345
|
+
lambda{|v| v.blank? ? nil : type.receive(v) }
|
346
|
+
else
|
347
|
+
raise("Can't receive #{type} #{info}")
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
def type_to_klass(type)
|
352
|
+
case
|
353
|
+
when type.is_a?(Class) then return type
|
354
|
+
when TYPE_ALIASES.has_key?(type) then TYPE_ALIASES[type]
|
355
|
+
# when (type.is_a?(Symbol) && type.to_s =~ /^[A-Z]/) then type.to_s.constantize
|
356
|
+
else raise ArgumentError, "Can\'t handle type #{type}: is it a Class or one of the TYPE_ALIASES?"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def to_tuple
|
362
|
+
tuple = []
|
363
|
+
self.each_value do |val|
|
364
|
+
if val.respond_to?(:to_tuple)
|
365
|
+
tuple += val.to_tuple
|
366
|
+
else
|
367
|
+
tuple << val
|
368
|
+
end
|
369
|
+
end
|
370
|
+
tuple
|
371
|
+
end
|
372
|
+
|
373
|
+
module ClassMethods
|
374
|
+
# By default, the hashlike methods iterate over the receiver attributes.
|
375
|
+
# If you want to filter our add to the keys list, override this method
|
376
|
+
#
|
377
|
+
# @example
|
378
|
+
# def self.members
|
379
|
+
# super + [:firstname, :lastname] - [:fullname]
|
380
|
+
# end
|
381
|
+
#
|
382
|
+
def members
|
383
|
+
receiver_attr_names
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
# set up receiver attributes, and bring in methods from the ClassMethods module at class-level
|
388
|
+
def self.included base
|
389
|
+
base.class_eval do
|
390
|
+
unless method_defined?(:receiver_attrs)
|
391
|
+
class_attribute :receiver_attrs
|
392
|
+
class_attribute :receiver_attr_names
|
393
|
+
class_attribute :after_receivers
|
394
|
+
self.receiver_attrs = {} # info about the attr
|
395
|
+
self.receiver_attr_names = [] # ordered set of attr names
|
396
|
+
self.after_receivers = [] # blocks to execute following receive!
|
397
|
+
extend ClassMethods
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|