notch8-alter-ego 1.0.1 → 1.0.2

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