function_chain 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,297 @@
1
+ require "function_chain/base_chain"
2
+
3
+ module FunctionChain
4
+ # == PullChain
5
+ # PullChain is object as represent method call chain.
6
+ # Can inner object's method call of object.
7
+ #
8
+ # Chain is object, so can call later.
9
+ #
10
+ # Supported call chain type is like a
11
+ # account.user.name
12
+ #
13
+ # Unsupported call chain type is like a
14
+ # filter3(filter2(filter1(value)))
15
+ # (RelayChain to support such type.)
16
+ #
17
+ # === Example
18
+ # Account = Struct.new(:user)
19
+ # User = Struct.new(:name)
20
+ # account = Account.new(User.new("Louis"))
21
+ #
22
+ # chain = PullChain.new(account, :user, :name, :upcase)
23
+ # chain.call # => LOUIS
24
+ #
25
+ # similar.
26
+ # # Strings separated by a slash
27
+ # PullChain.new(account, "user/name/upcase").call
28
+ #
29
+ # # use << operator.
30
+ # chain = PullChain.new(account)
31
+ # chain << :user << :name << :upcase
32
+ # chain.call
33
+ #
34
+ # # use add method.
35
+ # chain.add(:user).add(:name).add(:upcase).call
36
+ #
37
+ # # use add_all method.
38
+ # chain.add_all(:user, :name, :upcase).call
39
+ #
40
+ # can exist nil value on the way, like a following case.
41
+ # user.name = nil
42
+ # chain.call # => nil
43
+ #
44
+ # insert, insert_all method is insert_all method to chain.
45
+ # delete_at method is delete method from chain.
46
+ # clear method is delete all method from chain.
47
+ #
48
+ # === Require arguments on method
49
+ # Following example is required two arguments.
50
+ # class Foo
51
+ # def say(speaker, message)
52
+ # puts "#{speaker} said '#{message}'"
53
+ # end
54
+ # end
55
+ #
56
+ # Solution1:Array, format is [Symbol, [*Args]].
57
+ # chain = PullChain.new(Foo.new) << [:say, ["Andres", "Hello"]]
58
+ # chain.call => Andres said 'Hello'
59
+ #
60
+ # Solution2:String
61
+ # chain = PullChain.new(foo) << "say('John', 'Goodbye')"
62
+ # chain.call => John said 'Goodbye'
63
+ #
64
+ # === Require block on method
65
+ # [1,2,3,4,5].inject(3) { |sum, n| sum + n } # => 18
66
+ #
67
+ # Solution1:Array, format is [Symbol, [*Args, Proc]].
68
+ # chain = PullChain.new([1,2,3,4,5])
69
+ # chain << [:inject, [3, lambda { |sum, n| sum + n }]]
70
+ # chain.call # => 18
71
+ #
72
+ # Solution2:String
73
+ # chain = PullChain.new([1,2,3,4,5])
74
+ # chain << "inject(3) { |sum, n| sum + n }"
75
+ # chain.call # => 18
76
+ #
77
+ # === Use result on chain
78
+ # Like a following example, can use result on chain.
79
+ # Example1:String
80
+ # Foo = Struct.new(:bar)
81
+ # Bar = Struct.new(:baz) {
82
+ # def speaker ; "Julian" end
83
+ # }
84
+ # class Baz
85
+ # def say(speaker, message) puts "#{speaker} said '#{message}'" end
86
+ # end
87
+ # foo = Foo.new(Bar.new(Baz.new))
88
+ #
89
+ # # can use bar instance in backward!
90
+ # chain = PullChain.new(foo) << "bar/baz/say(bar.speaker, 'Good!')"
91
+ # chain.call # => Julian said 'Good!'
92
+ #
93
+ # furthermore, can use variable name assigned.
94
+ # # @b is bar instance alias.
95
+ # chain = PullChain.new(foo) << "@b = bar/baz/say(b.speaker, 'Cool')"
96
+ # chain.call # => Julian said 'Cool'
97
+ #
98
+ # Example2:Array
99
+ # can access result by Proc.
100
+ # chain = PullChain.new(foo) << :bar << :baz
101
+ # chain << [:say, Proc.new { next bar.speaker, "Oh" }]
102
+ # chain.call # => Julian said 'Oh'
103
+ #
104
+ # case of use a lambda, can use result access object explicit.
105
+ # chain = PullChain.new(foo) << :bar << :baz
106
+ # arg_reader = lambda { |accessor| next accessor.bar.speaker, "Oh" }
107
+ # chain << [:say, arg_reader]
108
+ # chain.call # => Julian said 'Oh'
109
+ #
110
+ # === etc
111
+ # How to use slash in strings separated by a slash.
112
+ # like following, please escaped by backslash.
113
+ # chain = PullChain.new("AC") << "concat '\\/DC'"
114
+ # chain.call # => AC/DC
115
+ #
116
+ # Use return_nil_at_error= method, then can ignore error.
117
+ # chain = PullChain.new("Test") << :xxx
118
+ # begin
119
+ # chain.call # => undefined method `xxx'
120
+ # rescue
121
+ # end
122
+ # chain.return_nil_at_error = true
123
+ # chain.call # => nil
124
+ #
125
+ # Note:use operator in string type chain
126
+ # table = {name: %w(Bill Scott Paul)}
127
+ # PullChain.new(table, "[:name]").call # NG
128
+ # PullChain.new(table, "self[:name]").call # OK
129
+ # # Array type chain
130
+ # PullChain.new(table, [:[], [:name]]).call # OK
131
+ #
132
+ # following is also the same.
133
+ # # <<operator of String
134
+ # PullChain.new("Led", "self << ' Zeppelin'").call
135
+ # # []operator of Array
136
+ # PullChain.new(%w(Donald Walter), "self[1]").call
137
+ #
138
+ # Some classes, such Fixnum and Bignum not supported.
139
+ # # NG
140
+ # PullChain.new(999999999999999, "self % 2").call
141
+ #
142
+ class PullChain < BaseChain
143
+ attr_writer :return_nil_at_error
144
+
145
+ # Initialize chain
146
+ #
147
+ # initialize(receiver, *functions)
148
+ # receiver: starting point of method call.
149
+ # *functions: more than one symbol, string, array.
150
+ def initialize(receiver, *functions)
151
+ @start_receiver = receiver
152
+ @return_nil_at_error = false
153
+ add_all(*functions)
154
+ end
155
+
156
+ # Call to all added method.
157
+ def call
158
+ @result_accessor = Object.new
159
+ begin
160
+ chain_elements.reduce(@start_receiver) do |receiver, chain_element|
161
+ break receiver if receiver.nil?
162
+ chain_element.call receiver
163
+ end
164
+ rescue
165
+ raise unless return_nil_at_error?
166
+ end
167
+ end
168
+
169
+ def return_nil_at_error?
170
+ @return_nil_at_error
171
+ end
172
+
173
+ private
174
+
175
+ attr_accessor :result_accessor
176
+
177
+ def create_common_chain_element(&block)
178
+ lambda do |receiver|
179
+ name, result = block.call(receiver)
180
+ define_result_access_method(name, result)
181
+ result
182
+ end
183
+ end
184
+
185
+ def define_result_access_method(name, result)
186
+ result_accessor.singleton_class.class_eval do
187
+ define_method name do
188
+ result
189
+ end
190
+ end
191
+ end
192
+
193
+ def create_chain_element_by_symbol(symbol)
194
+ create_common_chain_element do |receiver|
195
+ next symbol, execute(receiver, symbol)
196
+ end
197
+ end
198
+
199
+ def create_chain_element_by_array(array)
200
+ validate_array_length(array, 2, "symbol, [*args] or Proc")
201
+ validate_element_type_of_array(array, 1, [Array, Proc], "[*args] or Proc")
202
+
203
+ do_create_chain_element_by_array(array[0], array[1])
204
+ end
205
+
206
+ def do_create_chain_element_by_array(name, array_function_param)
207
+ create_common_chain_element do |receiver|
208
+ args, block = extract_args_and_block(array_function_param)
209
+ next name, execute(receiver, name, *args, &block)
210
+ end
211
+ end
212
+
213
+ def extract_args_and_block(array_function_param)
214
+ if array_function_param.is_a? Proc
215
+ return result_accessor.instance_eval(&array_function_param)
216
+ end
217
+ if array_function_param.last.is_a? Proc
218
+ return array_function_param[0...-1], array_function_param.last
219
+ end
220
+ array_function_param
221
+ end
222
+
223
+ def create_chain_element_by_string(string)
224
+ name, function = split_to_name_and_function(string)
225
+ create_common_chain_element do |receiver|
226
+ next name, execute_by_string(receiver, function)
227
+ end
228
+ end
229
+
230
+ def split_to_name_and_function(string)
231
+ name = string
232
+ function = string
233
+
234
+ md = string.match(/^@.+?=/)
235
+ if md
236
+ name = md[0][1...-1].strip
237
+ validate_variable_name_format(name)
238
+ function = string.sub(md[0], "").strip
239
+ end
240
+
241
+ return name, function
242
+ end
243
+
244
+ def validate_variable_name_format(name)
245
+ if name =~ /^[^a-zA-Z_]/
246
+ fail ArgumentError, "wrong format variable defined #{name}"
247
+ end
248
+ end
249
+
250
+ def execute(receiver, name, *args, &block)
251
+ receiver.__send__(name, *args, &block)
252
+ end
253
+
254
+ def execute_by_string(receiver, function)
255
+ begin
256
+ inject_result_accessor(receiver)
257
+ receiver.instance_eval(function)
258
+ rescue => ex
259
+ raise ex, "#{receiver}.#{function}"
260
+ ensure
261
+ eject_result_accessor(receiver)
262
+ end
263
+ end
264
+
265
+ def inject_result_accessor(receiver)
266
+ store_method_missing(receiver)
267
+ define_intercepted_method_missing(receiver)
268
+ end
269
+
270
+ def store_method_missing(receiver)
271
+ receiver.singleton_class.class_eval do
272
+ alias_method :original_method_missing, :method_missing
273
+ end
274
+ end
275
+
276
+ def define_intercepted_method_missing(receiver)
277
+ # interception method_missing
278
+ accessor = result_accessor
279
+ receiver.singleton_class.class_eval do
280
+ define_method :method_missing do |name, *args, &block|
281
+ super(name, *args, &block) unless accessor.respond_to? name
282
+ accessor.send(name, *args, &block)
283
+ end
284
+ end
285
+ end
286
+
287
+ def eject_result_accessor(receiver)
288
+ # cleanup to interception
289
+ receiver.singleton_class.class_eval do
290
+ alias_method :method_missing, :original_method_missing
291
+ undef_method :original_method_missing
292
+ end
293
+ end
294
+
295
+ alias_method :<<, :add
296
+ end
297
+ end
@@ -0,0 +1,251 @@
1
+ require "function_chain/base_chain"
2
+
3
+ module FunctionChain
4
+ # == RelayChain
5
+ # RelayChain is object like a connect to
6
+ # function's input from function's output.
7
+ # (methods well as can connect Proc.)
8
+ #
9
+ # Chain is object, so can call later.
10
+ #
11
+ # Supported call chain type is like a
12
+ # filter3(filter2(filter1(value))).
13
+ #
14
+ # Unsupported call chain type is like a
15
+ # account.user.name
16
+ # (PullChain to support such type.)
17
+ #
18
+ # === Example
19
+ # class Decorator
20
+ # def decorate1(value)
21
+ # "( #{value} )"
22
+ # end
23
+ # def decorate2(value)
24
+ # "{ #{value} }"
25
+ # end
26
+ # end
27
+ # chain = RelayChain.new(Decorator.new, :decorate1, :decorate2)
28
+ # chain.call("Hello") # => { ( Hello ) }
29
+ #
30
+ # similar.
31
+ # # Strings separated by a slash
32
+ # chain = RelayChain.new(Decorator.new, "decorate1/decorate2")
33
+ # chain.call("Hello")
34
+ #
35
+ # # use >> operator.
36
+ # chain = RelayChain.new(Decorator.new)
37
+ # chain >> :decorate1 >> :decorate2
38
+ # chain.call("Hello")
39
+ #
40
+ # # use Method object
41
+ # chain = RelayChain.new
42
+ # chain >> decorator.method(:decorate1) >> decorator.method(:decorate2)
43
+ # chain.call("Hello")
44
+ #
45
+ # # use add method
46
+ # chain.add(:decorate1).add(:decorate2).call("Hello")
47
+ #
48
+ # # use add_all method
49
+ # chain.add_all(:decorate1, :decorate2).call("Hello")
50
+ #
51
+ # insert, insert_all method is insert function to chain.
52
+ # delete_at method is delete function from chain.
53
+ # clear method is delete all function from chain.
54
+ #
55
+ # === How to connect method of differed instance
56
+ # Example, following two class.
57
+ # Introduce how to connect method of these class.
58
+ # class Decorator
59
+ # def decorate1(value) "( #{value} )" end
60
+ # def decorate2(value) "{ #{value} }" end
61
+ # end
62
+ # class Decorator2
63
+ # def decorate(value) "[ #{value} ]" end
64
+ # end
65
+ #
66
+ # Solution1:Array, format is [instance, Symbol or String of method]
67
+ # chain = RelayChain.new(Decorator.new)
68
+ # chain >> :decorate1 >> :decorate2 >> [Decorator2.new, :decorate]
69
+ # # String ver.
70
+ # # chain >> :decorate1 >> :decorate2 >> [Decorator2.new, "decorate"]
71
+ # chain.call("Hello") # => [ { ( Hello ) } ]
72
+ #
73
+ # Solution2:String, use registered instance.
74
+ # chain = RelayChain.new(Decorator.new)
75
+ # # register name and instance
76
+ # chain.add_receiver("d2", Decorator2.new)
77
+ # # use registered instance
78
+ # chain >> "/decorate1/decorate2/d2.decorate"
79
+ # chain.call("Hello") # => [ { ( Hello ) } ]
80
+ #
81
+ # # add_receiver_table method is register name and instance at once.
82
+ # chain.add_receiver_table({"x" => X.new, "y" => Y.new})
83
+ #
84
+ # === Case of method's output and method's input mismatch
85
+ # Following example, decorate output is 1, and union input is 2.
86
+ # How to do connect these methods?
87
+ # class Decorator
88
+ # def decorate(value)
89
+ # "#{value} And"
90
+ # end
91
+ # def union(value1, value2)
92
+ # "#{value1} #{value2}"
93
+ # end
94
+ # end
95
+ #
96
+ # Solution1:define connect method.
97
+ # class Decorator
98
+ # def connect(value)
99
+ # return value, "Palmer"
100
+ # end
101
+ # end
102
+ # chain = RelayChain.new(Decorator.new)
103
+ # chain >> :decorate >> :connect >> :union
104
+ # chain.call("Emerson, Lake") # => Emerson, Lake And Palmer
105
+ #
106
+ # Solution2:add lambda or Proc to between these methods.
107
+ # lambda's format is following.
108
+ # lambda {|chain, *args| chain.call(next function's arguments) }.
109
+ # lambda's parameter:chain is chain object.
110
+ # lambda's parameter:*args is previous function's output.
111
+ # can call next function by chain object.
112
+ #
113
+ # chain = RelayChain.new(Decorator.new)
114
+ # arg_adder = lambda { |chain, value| chain.call(value, "Jerry") }
115
+ # chain >> :decorate >> arg_adder >> :union
116
+ # chain.call("Tom") # => Tom And Jerry
117
+ #
118
+ # === Appendix
119
+ # Chain stop by means of lambda.
120
+ #
121
+ # class Decorator
122
+ # def decorate1(value) "( #{value} )" end
123
+ # def decorate2(value) "{ #{value} }" end
124
+ # end
125
+ #
126
+ # def create_stopper(&stop_condition)
127
+ # lambda do |chain, value|
128
+ # # if stop conditions are met then return value
129
+ # if stop_condition.call(value)
130
+ # value
131
+ # else
132
+ # chain.call(value)
133
+ # end
134
+ # end
135
+ # end
136
+ #
137
+ # chain = RelayChain.new(Decorator.new, :decorate1, :decorate2)
138
+ #
139
+ # # insert_all conditional chain stopper
140
+ # chain.insert(1, create_stopper { |value| value =~ /\d/ })
141
+ # chain.call("Van Halen 1984") # => ( Van Halen 1984 ) not enclosed to {}
142
+ # chain.call("Van Halen Jump") # => { ( Van Halen Jump ) } enclosed to {}
143
+ #
144
+ class RelayChain < BaseChain
145
+ # Initialize chain
146
+ #
147
+ # initialize(common_receiver = nil, *functions)
148
+ # common_receiver:used if the instance is omitted
149
+ # *functions: more than one symbol, string, array, method, proc
150
+ def initialize(common_receiver = nil, *functions)
151
+ @common_receiver = common_receiver
152
+ @index = 0
153
+ add_all(*functions)
154
+ end
155
+
156
+ # Call to all added function.
157
+ def call(*args)
158
+ begin
159
+ return if last?
160
+ chain_element = chain_elements[@index]
161
+ @index += 1
162
+ chain_element.call(self, *args)
163
+ ensure
164
+ @index = 0
165
+ end
166
+ end
167
+
168
+ # Whether chain last
169
+ def last?
170
+ @index == chain_elements.length
171
+ end
172
+
173
+ # add receiver
174
+ # use by string type function.
175
+ #
176
+ # add_receiver(name, receiver)
177
+ # name:receiver's name
178
+ # receiver:register this receiver
179
+ def add_receiver(name, receiver)
180
+ receiver_table[name] = receiver
181
+ self
182
+ end
183
+
184
+ # register name and instance at once.
185
+ #
186
+ # add_receiver_table(table)
187
+ # table:hash {receiver's name as String => receiver, ...}
188
+ def add_receiver_table(table)
189
+ receiver_table.merge! table
190
+ self
191
+ end
192
+
193
+ protected
194
+
195
+ def supported_types
196
+ super() | [Method, Proc]
197
+ end
198
+
199
+ private
200
+
201
+ def create_chain_element(function)
202
+ case function
203
+ when Method then create_chain_element_by_method(function)
204
+ when Proc then function
205
+ else super(function)
206
+ end
207
+ end
208
+
209
+ def create_common_chain_element(&block)
210
+ lambda do |chain, *args|
211
+ chain.last? ? block.call(*args) : chain.call(*block.call(*args))
212
+ end
213
+ end
214
+
215
+ def create_chain_element_by_method(method)
216
+ create_common_chain_element { |*args| method.call(*args) }
217
+ end
218
+
219
+ def create_chain_element_by_array(arr)
220
+ validate_array_length(arr, 2, "receiver, symbol or string of receiver's method name")
221
+ validate_element_type_of_array(arr, 1, [Symbol, String], "[Symbol or String]")
222
+
223
+ create_common_chain_element { |*args| arr[0].__send__(arr[1], *args) }
224
+ end
225
+
226
+ def create_chain_element_by_symbol(symbol)
227
+ create_common_chain_element do |*args|
228
+ @common_receiver.__send__(symbol, *args)
229
+ end
230
+ end
231
+
232
+ def create_chain_element_by_string(string)
233
+ create_common_chain_element do |*args|
234
+ index = string.index(".")
235
+ if index
236
+ receiver_key = string[0...index]
237
+ receiver_method = string[index + 1..-1]
238
+ receiver_table[receiver_key].send(receiver_method, *args)
239
+ else
240
+ @common_receiver.__send__(string, *args)
241
+ end
242
+ end
243
+ end
244
+
245
+ def receiver_table
246
+ @receiver_table ||= {}
247
+ end
248
+
249
+ alias_method :>>, :add
250
+ end
251
+ end
@@ -0,0 +1,3 @@
1
+ module FunctionChain
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,13 @@
1
+ require "function_chain/version"
2
+ require "function_chain/pull_chain"
3
+ require "function_chain/relay_chain"
4
+
5
+ module FunctionChain
6
+ module_function
7
+
8
+ # Shortcut to
9
+ # PullChain.new(receiver, *functions).call
10
+ def pull(receiver, *functions)
11
+ PullChain.new(receiver, *functions).call
12
+ end
13
+ end