function_chain 0.0.1

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.
@@ -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