motion-support 0.2.3 → 0.2.4
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/lib/motion-support/callbacks.rb +9 -0
- data/lib/motion-support/concern.rb +7 -0
- data/motion/_stdlib/array.rb +13 -0
- data/motion/_stdlib/enumerable.rb +9 -0
- data/motion/callbacks.rb +511 -0
- data/motion/concern.rb +122 -0
- data/motion/core_ext/kernel/singleton_class.rb +6 -0
- data/motion/version.rb +1 -1
- data/spec/motion-support/callback_spec.rb +702 -0
- data/spec/motion-support/concern_spec.rb +93 -0
- data/spec/motion-support/core_ext/kernel/singleton_class_spec.rb +9 -0
- metadata +17 -4
data/motion/callbacks.rb
ADDED
@@ -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
|