notch8-alter-ego 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/alter_ego.rb +1 -1
  2. data/lib/hookr.rb +469 -0
  3. metadata +2 -1
data/lib/alter_ego.rb CHANGED
@@ -10,7 +10,7 @@ require 'fail_fast'
10
10
  require 'hookr'
11
11
 
12
12
  module AlterEgo
13
- VERSION = '1.0.0'
13
+ VERSION = '1.0.2'
14
14
 
15
15
  include FailFast::Assertions
16
16
 
data/lib/hookr.rb ADDED
@@ -0,0 +1,469 @@
1
+ require 'set'
2
+ require 'generator'
3
+ require 'rubygems'
4
+ require 'fail_fast'
5
+
6
+ # Hookr is a library providing "hooks", aka "signals and slots", aka "events" to
7
+ # your Ruby classes.
8
+ module Hookr
9
+
10
+ # Include this module to decorate your class with hookable goodness.
11
+ #
12
+ # Note: remember to call super() if you define your own self.inherited().
13
+ module Hooks
14
+ module ClassMethods
15
+ # Returns the hooks exposed by this class
16
+ def hooks
17
+ (@hooks ||= HookSet.new)
18
+ end
19
+
20
+ # Define a new hook +name+. If +params+ are supplied, they will become
21
+ # the hook's named parameters.
22
+ def define_hook(name, *params)
23
+ hooks << make_hook(name, nil, params)
24
+
25
+ # We must use string evaluation in order to define a method that can
26
+ # receive a block.
27
+ instance_eval(<<-END)
28
+ def #{name}(handle_or_method=nil, &block)
29
+ add_callback(:#{name}, handle_or_method, &block)
30
+ end
31
+ END
32
+ module_eval(<<-END)
33
+ def #{name}(handle=nil, &block)
34
+ add_external_callback(:#{name}, handle, block)
35
+ end
36
+ END
37
+ end
38
+
39
+ protected
40
+
41
+ def make_hook(name, parent, params)
42
+ Hook.new(name, parent, params)
43
+ end
44
+
45
+ private
46
+
47
+ def inherited(child)
48
+ child.instance_variable_set(:@hooks, hooks.deep_copy)
49
+ end
50
+
51
+ end # end of ClassMethods
52
+
53
+ # These methods are used at both the class and instance level
54
+ module CallbackHelpers
55
+ public
56
+
57
+ def remove_callback(hook_name, handle_or_index)
58
+ hooks[hook_name].remove_callback(handle_or_index)
59
+ end
60
+
61
+ protected
62
+
63
+ # Add a callback to a named hook
64
+ def add_callback(hook_name, handle_or_method=nil, &block)
65
+ if block
66
+ add_block_callback(hook_name, handle_or_method, block)
67
+ else
68
+ add_method_callback(hook_name, handle_or_method)
69
+ end
70
+ end
71
+
72
+ # Add a callback which will be executed
73
+ def add_wildcard_callback(handle=nil, &block)
74
+ hooks[:__wildcard__].add_external_callback(handle, &block)
75
+ end
76
+
77
+ # Remove a wildcard callback
78
+ def remove_wildcard_callback(handle_or_index)
79
+ remove_callback(:__wildcard__, handle_or_index)
80
+ end
81
+
82
+ private
83
+
84
+ # Add either an internal or external callback depending on the arity of
85
+ # the given +block+
86
+ def add_block_callback(hook_name, handle, block)
87
+ case block.arity
88
+ when -1, 0
89
+ hooks[hook_name].add_internal_callback(handle, &block)
90
+ else
91
+ add_external_callback(hook_name, handle, block)
92
+ end
93
+ end
94
+
95
+ # Add a callback which will be executed in the context from which it was defined
96
+ def add_external_callback(hook_name, handle, block)
97
+ hooks[hook_name].add_external_callback(handle, &block)
98
+ end
99
+
100
+ # Add a callback which will call an instance method of the source class
101
+ def add_method_callback(hook_name, method)
102
+ hooks[hook_name].add_method_callback(self, method)
103
+ end
104
+ end # end of CallbackHelpers
105
+
106
+ def self.included(other)
107
+ other.extend(ClassMethods)
108
+ other.extend(CallbackHelpers)
109
+ other.send(:include, CallbackHelpers)
110
+ other.send(:define_hook, :__wildcard__)
111
+ end
112
+
113
+ # returns the hooks exposed by this object
114
+ def hooks
115
+ (@hooks ||= self.class.hooks.deep_copy)
116
+ end
117
+
118
+ # Execute all callbacks associated with the hook identified by +hook_name+,
119
+ # plus any wildcard callbacks.
120
+ #
121
+ # When a block is supplied, this method functions differently. In that case
122
+ # the callbacks are executed recursively. The most recently defined callback
123
+ # is executed and passed an event and a set of arguments. Calling
124
+ # event.next will pass execution to the next most recently added callback,
125
+ # which again will be passed an event with a reference to the next callback,
126
+ # and so on. When the list of callbacks are exhausted, the +block+ is
127
+ # executed as if it too were a callback. If at any point event.next is
128
+ # passed arguments, they will replace the value of the callback arguments
129
+ # for callbacks further down the chain.
130
+ #
131
+ # In this way you can use callbacks as "around" advice to a block of
132
+ # code. For instance:
133
+ #
134
+ # execute_hook(:write_data, data) do |data|
135
+ # write(data)
136
+ # end
137
+ #
138
+ # Here, the code exposes a :write_data hook. Any callbacks attached to the
139
+ # hook will "wrap" the data writing event. Callbacks might log when the
140
+ # data writing operation was started and stopped, or they might encrypt the
141
+ # data before it is written, etc.
142
+ def execute_hook(hook_name, *args, &block)
143
+ event = Event.new(self, hook_name, args, !!block)
144
+
145
+ if block
146
+ execute_hook_recursively(hook_name, event, block)
147
+ else
148
+ execute_hook_iteratively(hook_name, event)
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def execute_hook_recursively(hook_name, event, block)
155
+ event.callbacks = callback_generator(hook_name, block)
156
+ event.next
157
+ end
158
+
159
+ def execute_hook_iteratively(hook_name, event)
160
+ hooks[:__wildcard__].execute_callbacks(event)
161
+ hooks[hook_name].execute_callbacks(event)
162
+ end
163
+
164
+ # Returns a Generator which yields:
165
+ # 1. Wildcard callbacks, in reverse order, followed by
166
+ # 2. +hook_name+ callbacks, in reverse order, followed by
167
+ # 3. a proc which delegates to +block+
168
+ #
169
+ # Intended for use with recursive hook execution.
170
+ #
171
+ # TODO: Some of this should probably be pushed down into Hookr::Hook.
172
+ def callback_generator(hook_name, block)
173
+ Generator.new do |g|
174
+ hooks[:__wildcard__].callbacks.to_a.reverse.each do |callback|
175
+ g.yield callback
176
+ end
177
+ hooks[hook_name].callbacks.to_a.reverse.each do |callback|
178
+ g.yield callback
179
+ end
180
+ g.yield(lambda do |event|
181
+ block.call(*event.arguments)
182
+ end)
183
+ end
184
+ end
185
+ end
186
+
187
+ # A single named hook
188
+ Hook = Struct.new(:name, :parent, :params) do
189
+ include FailFast::Assertions
190
+
191
+ def initialize(name, parent=nil, params=[])
192
+ assert(Symbol === name)
193
+ @handles = {}
194
+ super(name, parent || NullHook.new, params)
195
+ end
196
+
197
+ def initialize_copy(original)
198
+ self.name = original.name
199
+ self.parent = original
200
+ self.params = original.params
201
+ @callbacks = CallbackSet.new
202
+ end
203
+
204
+ def ==(other)
205
+ name == other.name
206
+ end
207
+
208
+ def eql?(other)
209
+ self.class == other.class && name == other.name
210
+ end
211
+
212
+ def hash
213
+ name.hash
214
+ end
215
+
216
+ def callbacks
217
+ (@callbacks ||= CallbackSet.new)
218
+ end
219
+
220
+ # Add a callback which will be executed in the context where it was defined
221
+ def add_external_callback(handle=nil, &block)
222
+ if block.arity > -1 && block.arity < params.size
223
+ raise ArgumentError, "Callback has incompatible arity"
224
+ end
225
+ add_block_callback(Hookr::ExternalCallback, handle, &block)
226
+ end
227
+
228
+ # Add a callback which will be executed in the context of the event source
229
+ def add_internal_callback(handle=nil, &block)
230
+ add_block_callback(Hookr::InternalCallback, handle, &block)
231
+ end
232
+
233
+ # Add a callback which will send the given +message+ to the event source
234
+ def add_method_callback(klass, message)
235
+ method = klass.instance_method(message)
236
+ add_callback(Hookr::MethodCallback.new(message, method, next_callback_index))
237
+ end
238
+
239
+ def add_callback(callback)
240
+ callbacks << callback
241
+ callback.handle
242
+ end
243
+
244
+ def remove_callback(handle_or_index)
245
+ case handle_or_index
246
+ when Symbol then callbacks.delete_if{|cb| cb.handle == handle_or_index}
247
+ when Integer then callbacks.delete_if{|cb| cb.index == handle_or_index}
248
+ else raise ArgumentError, "Key must be integer index or symbolic handle"
249
+ end
250
+ end
251
+
252
+ # Excute the callbacks in order. +source+ is the object initiating the event.
253
+ def execute_callbacks(event)
254
+ parent.execute_callbacks(event)
255
+ callbacks.execute(event)
256
+ end
257
+
258
+ # Callback count including parents
259
+ def total_callbacks
260
+ callbacks.size + parent.total_callbacks
261
+ end
262
+
263
+ private
264
+
265
+ def next_callback_index
266
+ return 0 if callbacks.empty?
267
+ callbacks.map{|cb| cb.index}.max + 1
268
+ end
269
+
270
+ def add_block_callback(type, handle=nil, &block)
271
+ assert_exists(block)
272
+ assert(handle.nil? || Symbol === handle)
273
+ handle ||= next_callback_index
274
+ add_callback(type.new(handle, block, next_callback_index))
275
+ end
276
+ end
277
+
278
+ # A null object class for terminating Hook inheritance chains
279
+ class NullHook
280
+ def execute_callbacks(event)
281
+ # NOOP
282
+ end
283
+
284
+ def total_callbacks
285
+ 0
286
+ end
287
+ end
288
+
289
+ class HookSet < Set
290
+ WILDCARD_HOOK = Hookr::Hook.new(:__wildcard__)
291
+
292
+ # Find hook by name.
293
+ #
294
+ # TODO: Optimize this.
295
+ def [](key)
296
+ detect {|v| v.name == key} or raise IndexError, "No such hook: #{key}"
297
+ end
298
+
299
+ def deep_copy
300
+ result = HookSet.new
301
+ each do |hook|
302
+ result << hook.dup
303
+ end
304
+ result
305
+ end
306
+
307
+ # Length minus the wildcard hook (if any)
308
+ def length
309
+ if include?(WILDCARD_HOOK)
310
+ super - 1
311
+ else
312
+ super
313
+ end
314
+ end
315
+ end
316
+
317
+ class CallbackSet < SortedSet
318
+
319
+ # Fetch callback by either index or handle
320
+ def [](index)
321
+ case index
322
+ when Integer then detect{|cb| cb.index == index}
323
+ when Symbol then detect{|cb| cb.handle == index}
324
+ else raise ArgumentError, "index must be Integer or Symbol"
325
+ end
326
+ end
327
+
328
+ # get the first callback
329
+ def first
330
+ each do |cb|
331
+ return cb
332
+ end
333
+ end
334
+
335
+ def execute(event)
336
+ each do |callback|
337
+ callback.call(event)
338
+ end
339
+ end
340
+ end
341
+
342
+ Callback = Struct.new(:handle, :index) do
343
+ include Comparable
344
+ include FailFast::Assertions
345
+
346
+ # Callbacks with the same handle are always equal, which prevents duplicate
347
+ # handles in CallbackSets. Otherwise, callbacks are sorted by index.
348
+ def <=>(other)
349
+ if handle == other.handle
350
+ return 0
351
+ end
352
+ self.index <=> other.index
353
+ end
354
+
355
+ # Must be overridden in subclass
356
+ def call(*args)
357
+ raise NotImplementedError, "Callback is an abstract class"
358
+ end
359
+ end
360
+
361
+ # A base class for callbacks which execute a block
362
+ class BlockCallback < Callback
363
+ attr_reader :block
364
+
365
+ def initialize(handle, block, index)
366
+ @block = block
367
+ super(handle, index)
368
+ end
369
+ end
370
+
371
+ # A callback which will execute outside the event source
372
+ class ExternalCallback < BlockCallback
373
+ def call(event)
374
+ block.call(*event.to_args(block.arity))
375
+ end
376
+ end
377
+
378
+ # A callback which will execute in the context of the event source
379
+ class InternalCallback < BlockCallback
380
+ def initialize(handle, block, index)
381
+ assert(block.arity <= 0)
382
+ super(handle, block, index)
383
+ end
384
+
385
+ def call(event)
386
+ event.source.instance_eval(&block)
387
+ end
388
+ end
389
+
390
+ # A callback which will call a method on the event source
391
+ class MethodCallback < Callback
392
+ attr_reader :method
393
+
394
+ def initialize(handle, method, index)
395
+ @method = method
396
+ super(handle, index)
397
+ end
398
+
399
+ def call(event)
400
+ method.bind(event.source).call(*event.to_args(method.arity))
401
+ end
402
+ end
403
+
404
+ # Represents an event which is triggering callbacks.
405
+ #
406
+ # +source+:: The object triggering the event.
407
+ # +name+:: The name of the event
408
+ # +arguments+:: Any arguments passed associated with the event
409
+ Event = Struct.new(:source, :name, :arguments, :recursive, :callbacks) do
410
+ include FailFast::Assertions
411
+
412
+ # Convert to arguments for a callback of the given arity. Given an event
413
+ # with three arguments, the rules are as follows:
414
+ #
415
+ # 1. If arity is -1 (meaning any number of arguments), or 4, the result will
416
+ # be [event, +arguments[0]+, +arguments[1]+, +arguments[2]+]
417
+ # 2. If arity is 3, the result will just be +arguments+
418
+ # 3. If arity is < 3, an error will be raised.
419
+ #
420
+ # Notice that as the arity is reduced, the event argument is trimmed off.
421
+ # However, it is not permitted to generate a subset of the +arguments+ list.
422
+ # If the arity is too small to allow all arguments to be passed, the method
423
+ # fails.
424
+ def to_args(arity)
425
+ case arity
426
+ when -1
427
+ full_arguments
428
+ when (min_argument_count..full_argument_count)
429
+ full_arguments.slice(full_argument_count - arity, arity)
430
+ else
431
+ raise ArgumentError, "Arity must be between #{min_argument_count} "\
432
+ "and #{full_argument_count}"
433
+ end
434
+ end
435
+
436
+ # This method, along with the callback generator defined in Hook,
437
+ # implements recursive callback execution.
438
+ #
439
+ # TODO: Consider making the next() automatically if the callback doesn't
440
+ # call it explicitly.
441
+ #
442
+ # TODO: Consider adding a cancel() method, implementation TBD.
443
+ def next(*args)
444
+ assert(recursive, callbacks)
445
+ event = self.class.new(source, name, arguments, recursive, callbacks)
446
+ event.arguments = args unless args.empty?
447
+ if callbacks.next?
448
+ callbacks.next.call(event)
449
+ else
450
+ raise "No more callbacks!"
451
+ end
452
+ end
453
+
454
+ private
455
+
456
+ def full_argument_count
457
+ full_arguments.size
458
+ end
459
+
460
+ def min_argument_count
461
+ arguments.size
462
+ end
463
+
464
+ def full_arguments
465
+ @full_arguments ||= [self, *arguments]
466
+ end
467
+ end
468
+
469
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notch8-alter-ego
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Avdi Grimm
@@ -65,6 +65,7 @@ files:
65
65
  - lib/alter_ego.rb
66
66
  - lib/hash.rb
67
67
  - lib/hash/keys.rb
68
+ - lib/hookr.rb
68
69
  - script/console
69
70
  - script/destroy
70
71
  - script/generate