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