motion-support 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require 'motion-require'
2
+
3
+ files = [
4
+ "concern",
5
+ "descendants_tracker",
6
+ "callbacks"
7
+ ].map { |file| File.expand_path(File.join(File.dirname(__FILE__), "/../../motion", "#{file}.rb")) }
8
+
9
+ Motion::Require.all(files)
@@ -0,0 +1,7 @@
1
+ require 'motion-require'
2
+
3
+ files = [
4
+ "concern"
5
+ ].map { |file| File.expand_path(File.join(File.dirname(__FILE__), "/../../motion", "#{file}.rb")) }
6
+
7
+ Motion::Require.all(files)
@@ -0,0 +1,13 @@
1
+ class Array
2
+ def reverse_each
3
+ return to_enum(:reverse_each) unless block_given?
4
+
5
+ i = size - 1
6
+ while i >= 0
7
+ yield self[i]
8
+ i -= 1
9
+ end
10
+
11
+ self
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Enumerable
2
+ def reverse_each(&block)
3
+ return to_enum(:reverse_each) unless block_given?
4
+
5
+ # There is no other way then to convert to an array first... see 1.9's source.
6
+ to_a.reverse_each(&block)
7
+ self
8
+ end
9
+ end
@@ -0,0 +1,511 @@
1
+ motion_require 'concern'
2
+
3
+ module MotionSupport
4
+ # Callbacks are code hooks that are run at key points in an object's lifecycle.
5
+ # The typical use case is to have a base class define a set of callbacks
6
+ # relevant to the other functionality it supplies, so that subclasses can
7
+ # install callbacks that enhance or modify the base functionality without
8
+ # needing to override or redefine methods of the base class.
9
+ #
10
+ # Mixing in this module allows you to define the events in the object's
11
+ # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+),
12
+ # set the instance methods, procs, or callback objects to be called (via
13
+ # +ClassMethods.set_callback+), and run the installed callbacks at the
14
+ # appropriate times (via +run_callbacks+).
15
+ #
16
+ # Three kinds of callbacks are supported: before callbacks, run before a
17
+ # certain event; after callbacks, run after the event; and around callbacks,
18
+ # blocks that surround the event, triggering it when they yield. Callback code
19
+ # can be contained in instance methods, procs or lambdas, or callback objects
20
+ # that respond to certain predetermined methods. See +ClassMethods.set_callback+
21
+ # for details.
22
+ #
23
+ # class Record
24
+ # include MotionSupport::Callbacks
25
+ # define_callbacks :save
26
+ #
27
+ # def save
28
+ # run_callbacks :save do
29
+ # puts "- save"
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ # class PersonRecord < Record
35
+ # set_callback :save, :before, :saving_message
36
+ # def saving_message
37
+ # puts "saving..."
38
+ # end
39
+ #
40
+ # set_callback :save, :after do |object|
41
+ # puts "saved"
42
+ # end
43
+ # end
44
+ #
45
+ # person = PersonRecord.new
46
+ # person.save
47
+ #
48
+ # Output:
49
+ # saving...
50
+ # - save
51
+ # saved
52
+ module Callbacks
53
+ extend Concern
54
+
55
+ included do
56
+ extend MotionSupport::DescendantsTracker
57
+ end
58
+
59
+ # Runs the callbacks for the given event.
60
+ #
61
+ # Calls the before and around callbacks in the order they were set, yields
62
+ # the block (if given one), and then runs the after callbacks in reverse
63
+ # order.
64
+ #
65
+ # If the callback chain was halted, returns +false+. Otherwise returns the
66
+ # result of the block, or +true+ if no block is given.
67
+ #
68
+ # run_callbacks :save do
69
+ # save
70
+ # end
71
+ def run_callbacks(kind, &block)
72
+ runner_name = self.class.__define_callbacks(kind, self)
73
+ send(runner_name, &block)
74
+ end
75
+
76
+ private
77
+
78
+ # A hook invoked everytime a before callback is halted.
79
+ # This can be overridden in AS::Callback implementors in order
80
+ # to provide better debugging/logging.
81
+ def halted_callback_hook(filter)
82
+ end
83
+
84
+ class Callback #:nodoc:#
85
+ @@_callback_sequence = 0
86
+
87
+ attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter
88
+
89
+ def initialize(chain, filter, kind, options, klass)
90
+ @chain, @kind, @klass = chain, kind, klass
91
+ normalize_options!(options)
92
+
93
+ @raw_filter, @options = filter, options
94
+ @filter = _compile_filter(filter)
95
+ recompile_options!
96
+ end
97
+
98
+ def clone(chain, klass)
99
+ obj = super()
100
+ obj.chain = chain
101
+ obj.klass = klass
102
+ obj.options = @options.dup
103
+ obj.options[:if] = @options[:if].dup
104
+ obj.options[:unless] = @options[:unless].dup
105
+ obj
106
+ end
107
+
108
+ def normalize_options!(options)
109
+ options[:if] = Array(options[:if])
110
+ options[:unless] = Array(options[:unless])
111
+ end
112
+
113
+ def name
114
+ chain.name
115
+ end
116
+
117
+ def next_id
118
+ @@_callback_sequence += 1
119
+ end
120
+
121
+ def matches?(_kind, _filter)
122
+ @kind == _kind && @raw_filter == _filter
123
+ end
124
+
125
+ def duplicates?(other)
126
+ matches?(other.kind, other.raw_filter)
127
+ end
128
+
129
+ def _update_filter(filter_options, new_options)
130
+ filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless)
131
+ filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if)
132
+ end
133
+
134
+ def recompile!(_options)
135
+ _update_filter(self.options, _options)
136
+
137
+ recompile_options!
138
+ end
139
+
140
+ # Wraps code with filter
141
+ def apply(code)
142
+ case @kind
143
+ when :before
144
+ lambda do |obj, value, halted|
145
+ if !halted && @compiled_options.call(obj)
146
+ result = @filter.call(obj)
147
+
148
+ halted = chain.config[:terminator].call(result)
149
+ if halted
150
+ obj.halted_callback_hook(@raw_filter.inspect)
151
+ end
152
+ end
153
+ code.call(obj, value, halted)
154
+ end
155
+ when :after
156
+ lambda do |obj, value, halted|
157
+ value, halted = *(code.call(obj, value, halted))
158
+ if (!chain.config[:skip_after_callbacks_if_terminated] || !halted) && @compiled_options.call(obj)
159
+ @filter.call(obj)
160
+ end
161
+ [value, halted]
162
+ end
163
+ when :around
164
+ lambda do |obj, value, halted|
165
+ if @compiled_options.call(obj) && !halted
166
+ @filter.call(obj) do
167
+ value, halted = *(code.call(obj, value, halted))
168
+ value
169
+ end
170
+ else
171
+ value, halted = *(code.call(obj, value, halted))
172
+ value
173
+ end
174
+ [value, halted]
175
+ end
176
+ end
177
+ end
178
+
179
+ private
180
+ # Options support the same options as filters themselves (and support
181
+ # symbols, string, procs, and objects), so compile a conditional
182
+ # expression based on the options.
183
+ def recompile_options!
184
+ @conditions = [ lambda { |obj| true } ]
185
+
186
+ unless options[:if].empty?
187
+ @conditions << Array(_compile_filter(options[:if]))
188
+ end
189
+
190
+ unless options[:unless].empty?
191
+ @conditions << Array(_compile_filter(options[:unless])).map {|f| lambda { |obj| !f.call(obj) } }
192
+ end
193
+
194
+ @compiled_options = lambda { |obj| @conditions.flatten.all? { |c| c.call(obj) } }
195
+ end
196
+
197
+ # Filters support:
198
+ #
199
+ # Arrays:: Used in conditions. This is used to specify
200
+ # multiple conditions. Used internally to
201
+ # merge conditions from skip_* filters.
202
+ # Symbols:: A method to call.
203
+ # Procs:: A proc to call with the object.
204
+ # Objects:: An object with a <tt>before_foo</tt> method on it to call.
205
+ #
206
+ # All of these objects are compiled into methods and handled
207
+ # the same after this point:
208
+ #
209
+ # Arrays:: Merged together into a single filter.
210
+ # Symbols:: Already methods.
211
+ # Procs:: define_method'ed into methods.
212
+ # Objects::
213
+ # a method is created that calls the before_foo method
214
+ # on the object.
215
+ def _compile_filter(filter)
216
+ case filter
217
+ when Array
218
+ lambda { |obj, &block| filter.all? {|f| _compile_filter(f).call(obj, &block) } }
219
+ when Symbol, String
220
+ lambda { |obj, &block| obj.send filter, &block }
221
+ when Proc
222
+ filter
223
+ else
224
+ method_name = "_callback_#{@kind}_#{next_id}"
225
+ @klass.send(:define_method, "#{method_name}_object") { filter }
226
+
227
+ scopes = Array(chain.config[:scope])
228
+ method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
229
+
230
+ @klass.class_eval do
231
+ define_method method_name do |&blk|
232
+ send("#{method_name}_object").send(method_to_call, self, &blk)
233
+ end
234
+ end
235
+
236
+ lambda { |obj, &block| obj.send method_name, &block }
237
+ end
238
+ end
239
+ end
240
+
241
+ # An Array with a compile method.
242
+ class CallbackChain < Array #:nodoc:#
243
+ attr_reader :name, :config
244
+
245
+ def initialize(name, config)
246
+ @name = name
247
+ @config = {
248
+ :terminator => lambda { |result| false },
249
+ :scope => [ :kind ]
250
+ }.merge!(config)
251
+ end
252
+
253
+ def compile
254
+ lambda do |obj, &block|
255
+ value = nil
256
+ halted = false
257
+
258
+ callbacks = lambda do |obj, value, halted|
259
+ value = !halted && (block.call if block)
260
+ [value, halted]
261
+ end
262
+
263
+ reverse_each do |callback|
264
+ callbacks = callback.apply(callbacks)
265
+ end
266
+
267
+ value, halted = *(callbacks.call(obj, value, halted))
268
+
269
+ value
270
+ end
271
+ end
272
+
273
+ def append(*callbacks)
274
+ callbacks.each { |c| append_one(c) }
275
+ end
276
+
277
+ def prepend(*callbacks)
278
+ callbacks.each { |c| prepend_one(c) }
279
+ end
280
+
281
+ private
282
+
283
+ def append_one(callback)
284
+ remove_duplicates(callback)
285
+ push(callback)
286
+ end
287
+
288
+ def prepend_one(callback)
289
+ remove_duplicates(callback)
290
+ unshift(callback)
291
+ end
292
+
293
+ def remove_duplicates(callback)
294
+ delete_if { |c| callback.duplicates?(c) }
295
+ end
296
+
297
+ end
298
+
299
+ module ClassMethods
300
+ # This method defines callback chain method for the given kind
301
+ # if it was not yet defined.
302
+ # This generated method plays caching role.
303
+ def __define_callbacks(kind, object) #:nodoc:
304
+ name = __callback_runner_name(kind)
305
+ unless object.respond_to?(name, true)
306
+ block = object.send("_#{kind}_callbacks").compile
307
+ define_method name do |&blk|
308
+ block.call(self, &blk)
309
+ end
310
+ protected name.to_sym
311
+ end
312
+ name
313
+ end
314
+
315
+ def __reset_runner(symbol)
316
+ name = __callback_runner_name(symbol)
317
+ undef_method(name) if method_defined?(name)
318
+ end
319
+
320
+ def __callback_runner_name_cache
321
+ @__callback_runner_name_cache ||= Hash.new {|cache, kind| cache[kind] = __generate_callback_runner_name(kind) }
322
+ end
323
+
324
+ def __generate_callback_runner_name(kind)
325
+ "_run__#{self.name.hash.abs}__#{kind}__callbacks"
326
+ end
327
+
328
+ def __callback_runner_name(kind)
329
+ __callback_runner_name_cache[kind]
330
+ end
331
+
332
+ # This is used internally to append, prepend and skip callbacks to the
333
+ # CallbackChain.
334
+ def __update_callbacks(name, filters = [], block = nil) #:nodoc:
335
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
336
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
337
+ filters.unshift(block) if block
338
+
339
+ ([self] + MotionSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
340
+ chain = target.send("_#{name}_callbacks")
341
+ yield target, chain.dup, type, filters, options
342
+ target.__reset_runner(name)
343
+ end
344
+ end
345
+
346
+ # Install a callback for the given event.
347
+ #
348
+ # set_callback :save, :before, :before_meth
349
+ # set_callback :save, :after, :after_meth, if: :condition
350
+ # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
351
+ #
352
+ # The second arguments indicates whether the callback is to be run +:before+,
353
+ # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
354
+ # means the first example above can also be written as:
355
+ #
356
+ # set_callback :save, :before_meth
357
+ #
358
+ # The callback can specified as a symbol naming an instance method; as a
359
+ # proc, lambda, or block; as a string to be instance evaluated; or as an
360
+ # object that responds to a certain method determined by the <tt>:scope</tt>
361
+ # argument to +define_callback+.
362
+ #
363
+ # If a proc, lambda, or block is given, its body is evaluated in the context
364
+ # of the current object. It can also optionally accept the current object as
365
+ # an argument.
366
+ #
367
+ # Before and around callbacks are called in the order that they are set;
368
+ # after callbacks are called in the reverse order.
369
+ #
370
+ # Around callbacks can access the return value from the event, if it
371
+ # wasn't halted, from the +yield+ call.
372
+ #
373
+ # ===== Options
374
+ #
375
+ # * <tt>:if</tt> - A symbol naming an instance method or a proc; the
376
+ # callback will be called only when it returns a +true+ value.
377
+ # * <tt>:unless</tt> - A symbol naming an instance method or a proc; the
378
+ # callback will be called only when it returns a +false+ value.
379
+ # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
380
+ # existing chain rather than appended.
381
+ def set_callback(name, *filter_list, &block)
382
+ mapped = nil
383
+
384
+ __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
385
+ mapped ||= filters.map do |filter|
386
+ Callback.new(chain, filter, type, options.dup, self)
387
+ end
388
+
389
+ options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
390
+
391
+ target.send("_#{name}_callbacks=", chain)
392
+ end
393
+ end
394
+
395
+ # Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or
396
+ # <tt>:unless</tt> options may be passed in order to control when the
397
+ # callback is skipped.
398
+ #
399
+ # class Writer < Person
400
+ # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 }
401
+ # end
402
+ def skip_callback(name, *filter_list, &block)
403
+ __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
404
+ filters.each do |filter|
405
+ filter = chain.find {|c| c.matches?(type, filter) }
406
+
407
+ if filter && options.any?
408
+ new_filter = filter.clone(chain, self)
409
+ chain.insert(chain.index(filter), new_filter)
410
+ new_filter.recompile!(options)
411
+ end
412
+
413
+ chain.delete(filter)
414
+ end
415
+ target.send("_#{name}_callbacks=", chain)
416
+ end
417
+ end
418
+
419
+ # Remove all set callbacks for the given event.
420
+ def reset_callbacks(symbol)
421
+ callbacks = send("_#{symbol}_callbacks")
422
+
423
+ MotionSupport::DescendantsTracker.descendants(self).each do |target|
424
+ chain = target.send("_#{symbol}_callbacks").dup
425
+ callbacks.each { |c| chain.delete(c) }
426
+ target.send("_#{symbol}_callbacks=", chain)
427
+ target.__reset_runner(symbol)
428
+ end
429
+
430
+ self.send("_#{symbol}_callbacks=", callbacks.dup.clear)
431
+
432
+ __reset_runner(symbol)
433
+ end
434
+
435
+ # Define sets of events in the object lifecycle that support callbacks.
436
+ #
437
+ # define_callbacks :validate
438
+ # define_callbacks :initialize, :save, :destroy
439
+ #
440
+ # ===== Options
441
+ #
442
+ # * <tt>:terminator</tt> - Determines when a before filter will halt the
443
+ # callback chain, preventing following callbacks from being called and
444
+ # the event from being triggered. This is a string to be eval'ed. The
445
+ # result of the callback is available in the +result+ variable.
446
+ #
447
+ # define_callbacks :validate, terminator: 'result == false'
448
+ #
449
+ # In this example, if any before validate callbacks returns +false+,
450
+ # other callbacks are not executed. Defaults to +false+, meaning no value
451
+ # halts the chain.
452
+ #
453
+ # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
454
+ # callbacks should be terminated by the <tt>:terminator</tt> option. By
455
+ # default after callbacks executed no matter if callback chain was
456
+ # terminated or not. Option makes sense only when <tt>:terminator</tt>
457
+ # option is specified.
458
+ #
459
+ # * <tt>:scope</tt> - Indicates which methods should be executed when an
460
+ # object is used as a callback.
461
+ #
462
+ # class Audit
463
+ # def before(caller)
464
+ # puts 'Audit: before is called'
465
+ # end
466
+ #
467
+ # def before_save(caller)
468
+ # puts 'Audit: before_save is called'
469
+ # end
470
+ # end
471
+ #
472
+ # class Account
473
+ # include MotionSupport::Callbacks
474
+ #
475
+ # define_callbacks :save
476
+ # set_callback :save, :before, Audit.new
477
+ #
478
+ # def save
479
+ # run_callbacks :save do
480
+ # puts 'save in main'
481
+ # end
482
+ # end
483
+ # end
484
+ #
485
+ # In the above case whenever you save an account the method
486
+ # <tt>Audit#before</tt> will be called. On the other hand
487
+ #
488
+ # define_callbacks :save, scope: [:kind, :name]
489
+ #
490
+ # would trigger <tt>Audit#before_save</tt> instead. That's constructed
491
+ # by calling <tt>#{kind}_#{name}</tt> on the given instance. In this
492
+ # case "kind" is "before" and "name" is "save". In this context +:kind+
493
+ # and +:name+ have special meanings: +:kind+ refers to the kind of
494
+ # callback (before/after/around) and +:name+ refers to the method on
495
+ # which callbacks are being defined.
496
+ #
497
+ # A declaration like
498
+ #
499
+ # define_callbacks :save, scope: [:name]
500
+ #
501
+ # would call <tt>Audit#save</tt>.
502
+ def define_callbacks(*callbacks)
503
+ config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
504
+ callbacks.each do |callback|
505
+ class_attribute "_#{callback}_callbacks"
506
+ send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
507
+ end
508
+ end
509
+ end
510
+ end
511
+ end