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.
- data/lib/alter_ego.rb +1 -1
- data/lib/hookr.rb +469 -0
- metadata +2 -1
data/lib/alter_ego.rb
CHANGED
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.
|
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
|