hookr 1.0.0
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/History.txt +4 -0
- data/Manifest.txt +22 -0
- data/README.txt +491 -0
- data/Rakefile +28 -0
- data/bin/hookr +8 -0
- data/lib/hookr.rb +653 -0
- data/spec/hookr_spec.rb +1041 -0
- data/spec/spec_helper.rb +16 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/gem.rake +192 -0
- data/tasks/git.rake +40 -0
- data/tasks/manifest.rake +48 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +39 -0
- data/tasks/rdoc.rake +50 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +279 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/test/test_hookr.rb +0 -0
- metadata +77 -0
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bones'
|
7
|
+
Bones.setup
|
8
|
+
rescue LoadError
|
9
|
+
load 'tasks/setup.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
ensure_in_path 'lib'
|
13
|
+
require 'hookr'
|
14
|
+
|
15
|
+
task :default => 'spec:run'
|
16
|
+
|
17
|
+
PROJ.name = 'hookr'
|
18
|
+
PROJ.authors = 'Avdi Grimm'
|
19
|
+
PROJ.email = 'avdi@avdi.org'
|
20
|
+
PROJ.url = 'http://hookr.rubyforge.org'
|
21
|
+
PROJ.version = HookR::VERSION
|
22
|
+
PROJ.rubyforge.name = 'hookr'
|
23
|
+
|
24
|
+
# TODO I want to be able to use -w here...
|
25
|
+
PROJ.ruby_opts = []
|
26
|
+
PROJ.spec.opts << '--color'
|
27
|
+
|
28
|
+
# EOF
|
data/bin/hookr
ADDED
data/lib/hookr.rb
ADDED
@@ -0,0 +1,653 @@
|
|
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
|
+
# No need to document the boilerplate convenience methods defined by Mr. Bones.
|
11
|
+
# :stopdoc:
|
12
|
+
|
13
|
+
VERSION = '1.0.0'
|
14
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
15
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
16
|
+
|
17
|
+
# Returns the version string for the library.
|
18
|
+
#
|
19
|
+
def self.version
|
20
|
+
VERSION
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the library path for the module. If any arguments are given,
|
24
|
+
# they will be joined to the end of the libray path using
|
25
|
+
# <tt>File.join</tt>.
|
26
|
+
#
|
27
|
+
def self.libpath( *args )
|
28
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the lpath for the module. If any arguments are given,
|
32
|
+
# they will be joined to the end of the path using
|
33
|
+
# <tt>File.join</tt>.
|
34
|
+
#
|
35
|
+
def self.path( *args )
|
36
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Utility method used to rquire all files ending in .rb that lie in the
|
40
|
+
# directory below this file that has the same name as the filename passed
|
41
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
42
|
+
# the _filename_ does not have to be equivalent to the directory.
|
43
|
+
#
|
44
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
45
|
+
dir ||= ::File.basename(fname, '.*')
|
46
|
+
search_me = ::File.expand_path(
|
47
|
+
::File.join(::File.dirname(fname), dir, '*', '*.rb'))
|
48
|
+
|
49
|
+
Dir.glob(search_me).sort.each {|rb| require rb}
|
50
|
+
end
|
51
|
+
|
52
|
+
# :startdoc:
|
53
|
+
|
54
|
+
# Include this module to decorate your class with hookable goodness.
|
55
|
+
#
|
56
|
+
# Note: remember to call super() if you define your own self.inherited().
|
57
|
+
module Hooks
|
58
|
+
module ClassMethods
|
59
|
+
# Returns the hooks exposed by this class
|
60
|
+
def hooks
|
61
|
+
result = fetch_or_create_hooks.dup.freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
# Define a new hook +name+. If +params+ are supplied, they will become
|
65
|
+
# the hook's named parameters.
|
66
|
+
def define_hook(name, *params)
|
67
|
+
fetch_or_create_hooks << make_hook(name, nil, params)
|
68
|
+
|
69
|
+
# We must use string evaluation in order to define a method that can
|
70
|
+
# receive a block.
|
71
|
+
instance_eval(<<-END)
|
72
|
+
def #{name}(handle_or_method=nil, &block)
|
73
|
+
add_callback(:#{name}, handle_or_method, &block)
|
74
|
+
end
|
75
|
+
END
|
76
|
+
module_eval(<<-END)
|
77
|
+
def #{name}(handle=nil, &block)
|
78
|
+
add_external_callback(:#{name}, handle, block)
|
79
|
+
end
|
80
|
+
END
|
81
|
+
end
|
82
|
+
|
83
|
+
def const_missing(const_name)
|
84
|
+
if const_name.to_s == "Listener"
|
85
|
+
hooks = fetch_or_create_hooks
|
86
|
+
listener_class ||= Class.new do
|
87
|
+
hooks.each do |hook|
|
88
|
+
define_method(hook.name) do |*args|
|
89
|
+
# NOOP
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
const_set(const_name, listener_class)
|
94
|
+
else
|
95
|
+
super(const_name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def make_hook(name, parent, params)
|
102
|
+
Hook.new(name, parent, params)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def inherited(child)
|
108
|
+
child.instance_variable_set(:@hooks, fetch_or_create_hooks.deep_copy)
|
109
|
+
end
|
110
|
+
|
111
|
+
def fetch_or_create_hooks
|
112
|
+
@hooks ||= HookSet.new
|
113
|
+
end
|
114
|
+
|
115
|
+
end # end of ClassMethods
|
116
|
+
|
117
|
+
# These methods are used at both the class and instance level
|
118
|
+
module CallbackHelpers
|
119
|
+
public
|
120
|
+
|
121
|
+
def remove_callback(hook_name, handle_or_index)
|
122
|
+
fetch_or_create_hooks[hook_name].remove_callback(handle_or_index)
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
# Add a callback to a named hook
|
128
|
+
def add_callback(hook_name, handle_or_method=nil, &block)
|
129
|
+
if block
|
130
|
+
add_block_callback(hook_name, handle_or_method, block)
|
131
|
+
else
|
132
|
+
add_method_callback(hook_name, handle_or_method)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Add a callback which will be executed
|
137
|
+
def add_wildcard_callback(handle=nil, &block)
|
138
|
+
fetch_or_create_hooks[:__wildcard__].add_basic_callback(handle, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove a wildcard callback
|
142
|
+
def remove_wildcard_callback(handle_or_index)
|
143
|
+
remove_callback(:__wildcard__, handle_or_index)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
# Add either an internal or external callback depending on the arity of
|
149
|
+
# the given +block+
|
150
|
+
def add_block_callback(hook_name, handle, block)
|
151
|
+
case block.arity
|
152
|
+
when -1, 0
|
153
|
+
fetch_or_create_hooks[hook_name].add_internal_callback(handle, &block)
|
154
|
+
else
|
155
|
+
add_external_callback(hook_name, handle, block)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Add a callback which will be executed in the context from which it was defined
|
160
|
+
def add_external_callback(hook_name, handle, block)
|
161
|
+
fetch_or_create_hooks[hook_name].add_external_callback(handle, &block)
|
162
|
+
end
|
163
|
+
|
164
|
+
def add_basic_callback(hook_name, handle, block)
|
165
|
+
fetch_or_create_hooks[hook_name].add_basic_callback(handle, &block)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Add a callback which will call an instance method of the source class
|
169
|
+
def add_method_callback(hook_name, method)
|
170
|
+
fetch_or_create_hooks[hook_name].add_method_callback(self, method)
|
171
|
+
end
|
172
|
+
|
173
|
+
end # end of CallbackHelpers
|
174
|
+
|
175
|
+
def self.included(other)
|
176
|
+
other.extend(ClassMethods)
|
177
|
+
other.extend(CallbackHelpers)
|
178
|
+
other.send(:include, CallbackHelpers)
|
179
|
+
other.send(:define_hook, :__wildcard__)
|
180
|
+
end
|
181
|
+
|
182
|
+
# returns the hooks exposed by this object
|
183
|
+
def hooks
|
184
|
+
fetch_or_create_hooks.dup.freeze
|
185
|
+
end
|
186
|
+
|
187
|
+
# Execute all callbacks associated with the hook identified by +hook_name+,
|
188
|
+
# plus any wildcard callbacks.
|
189
|
+
#
|
190
|
+
# When a block is supplied, this method functions differently. In that case
|
191
|
+
# the callbacks are executed recursively. The most recently defined callback
|
192
|
+
# is executed and passed an event and a set of arguments. Calling
|
193
|
+
# event.next will pass execution to the next most recently added callback,
|
194
|
+
# which again will be passed an event with a reference to the next callback,
|
195
|
+
# and so on. When the list of callbacks are exhausted, the +block+ is
|
196
|
+
# executed as if it too were a callback. If at any point event.next is
|
197
|
+
# passed arguments, they will replace the value of the callback arguments
|
198
|
+
# for callbacks further down the chain.
|
199
|
+
#
|
200
|
+
# In this way you can use callbacks as "around" advice to a block of
|
201
|
+
# code. For instance:
|
202
|
+
#
|
203
|
+
# execute_hook(:write_data, data) do |data|
|
204
|
+
# write(data)
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
# Here, the code exposes a :write_data hook. Any callbacks attached to the
|
208
|
+
# hook will "wrap" the data writing event. Callbacks might log when the
|
209
|
+
# data writing operation was started and stopped, or they might encrypt the
|
210
|
+
# data before it is written, etc.
|
211
|
+
def execute_hook(hook_name, *args, &block)
|
212
|
+
event = Event.new(self, hook_name, args, !!block)
|
213
|
+
|
214
|
+
if block
|
215
|
+
execute_hook_recursively(hook_name, event, block)
|
216
|
+
else
|
217
|
+
execute_hook_iteratively(hook_name, event)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Add a listener object. The object should have a method defined for every
|
222
|
+
# hook this object publishes.
|
223
|
+
def add_listener(listener, handle=listener_to_handle(listener))
|
224
|
+
add_wildcard_callback(handle) do |event|
|
225
|
+
listener.send(event.name, *event.arguments)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Remove a listener by handle or by the listener object itself
|
230
|
+
def remove_listener(handle_or_listener)
|
231
|
+
handle = case handle_or_listener
|
232
|
+
when Symbol then handle_or_listener
|
233
|
+
else listener_to_handle(handle_or_listener)
|
234
|
+
end
|
235
|
+
remove_wildcard_callback(handle)
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def execute_hook_recursively(hook_name, event, block)
|
241
|
+
event.callbacks = callback_generator(hook_name, block)
|
242
|
+
event.next
|
243
|
+
end
|
244
|
+
|
245
|
+
def execute_hook_iteratively(hook_name, event)
|
246
|
+
fetch_or_create_hooks[:__wildcard__].execute_callbacks(event)
|
247
|
+
fetch_or_create_hooks[hook_name].execute_callbacks(event)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns a Generator which yields:
|
251
|
+
# 1. Wildcard callbacks, in reverse order, followed by
|
252
|
+
# 2. +hook_name+ callbacks, in reverse order, followed by
|
253
|
+
# 3. a proc which delegates to +block+
|
254
|
+
#
|
255
|
+
# Intended for use with recursive hook execution.
|
256
|
+
def callback_generator(hook_name, block)
|
257
|
+
Generator.new do |g|
|
258
|
+
fetch_or_create_hooks[:__wildcard__].each_callback_reverse do |callback|
|
259
|
+
g.yield callback
|
260
|
+
end
|
261
|
+
fetch_or_create_hooks[hook_name].each_callback_reverse do |callback|
|
262
|
+
g.yield callback
|
263
|
+
end
|
264
|
+
g.yield(lambda do |event|
|
265
|
+
block.call(*event.arguments)
|
266
|
+
end)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def listener_to_handle(listener)
|
271
|
+
("listener_" + listener.object_id.to_s).to_sym
|
272
|
+
end
|
273
|
+
|
274
|
+
def fetch_or_create_hooks
|
275
|
+
@hooks ||= self.class.hooks.deep_copy
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# A single named hook
|
280
|
+
Hook = Struct.new(:name, :parent, :params) do
|
281
|
+
include FailFast::Assertions
|
282
|
+
|
283
|
+
def initialize(name, parent=nil, params=[])
|
284
|
+
assert(Symbol === name)
|
285
|
+
@handles = {}
|
286
|
+
super(name, parent || NullHook.new, params)
|
287
|
+
end
|
288
|
+
|
289
|
+
def initialize_copy(original)
|
290
|
+
self.name = original.name
|
291
|
+
self.parent = original
|
292
|
+
self.params = original.params
|
293
|
+
@callbacks = CallbackSet.new
|
294
|
+
end
|
295
|
+
|
296
|
+
def ==(other)
|
297
|
+
name == other.name
|
298
|
+
end
|
299
|
+
|
300
|
+
def eql?(other)
|
301
|
+
self.class == other.class && name == other.name
|
302
|
+
end
|
303
|
+
|
304
|
+
def hash
|
305
|
+
name.hash
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns false. Only true of NullHook.
|
309
|
+
def terminal?
|
310
|
+
false
|
311
|
+
end
|
312
|
+
|
313
|
+
# Returns true if this hook has a null parent
|
314
|
+
def root?
|
315
|
+
parent.terminal?
|
316
|
+
end
|
317
|
+
|
318
|
+
def callbacks
|
319
|
+
fetch_or_create_callbacks.dup.freeze
|
320
|
+
end
|
321
|
+
|
322
|
+
# Add a callback which will be executed in the context where it was defined
|
323
|
+
def add_external_callback(handle=nil, &block)
|
324
|
+
if block.arity > -1 && block.arity < params.size
|
325
|
+
raise ArgumentError, "Callback has incompatible arity"
|
326
|
+
end
|
327
|
+
add_block_callback(HookR::ExternalCallback, handle, &block)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Add a callback which will pass only the event object to +block+ - it will
|
331
|
+
# not try to pass arguments as well.
|
332
|
+
def add_basic_callback(handle=nil, &block)
|
333
|
+
add_block_callback(HookR::BasicCallback, handle, &block)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Add a callback which will be executed in the context of the event source
|
337
|
+
def add_internal_callback(handle=nil, &block)
|
338
|
+
add_block_callback(HookR::InternalCallback, handle, &block)
|
339
|
+
end
|
340
|
+
|
341
|
+
# Add a callback which will send the given +message+ to the event source
|
342
|
+
def add_method_callback(klass, message)
|
343
|
+
method = klass.instance_method(message)
|
344
|
+
add_callback(HookR::MethodCallback.new(message, method, next_callback_index))
|
345
|
+
end
|
346
|
+
|
347
|
+
def add_callback(callback)
|
348
|
+
fetch_or_create_callbacks << callback
|
349
|
+
callback.handle
|
350
|
+
end
|
351
|
+
|
352
|
+
def remove_callback(handle_or_index)
|
353
|
+
assert_exists(handle_or_index)
|
354
|
+
case handle_or_index
|
355
|
+
when Symbol then fetch_or_create_callbacks.delete_if{|cb| cb.handle == handle_or_index}
|
356
|
+
when Integer then fetch_or_create_callbacks.delete_if{|cb| cb.index == handle_or_index}
|
357
|
+
else raise ArgumentError, "Key must be integer index or symbolic handle "\
|
358
|
+
"(was: #{handle_or_index.inspect})"
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Empty this hook of callbacks. Parent hooks may still have callbacks.
|
363
|
+
def clear_callbacks!
|
364
|
+
fetch_or_create_callbacks.clear
|
365
|
+
end
|
366
|
+
|
367
|
+
# Empty this hook of its own AND parent callbacks. This also disconnects
|
368
|
+
# the hook from its parent, if any.
|
369
|
+
def clear_all_callbacks!
|
370
|
+
disconnect!
|
371
|
+
clear_callbacks!
|
372
|
+
end
|
373
|
+
|
374
|
+
# Yields callbacks in order of addition, starting with any parent hooks
|
375
|
+
def each_callback(&block)
|
376
|
+
parent.each_callback(&block)
|
377
|
+
fetch_or_create_callbacks.each(&block)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Yields callbacks in reverse order of addition, starting with own callbacks
|
381
|
+
# and then moving on to any parent hooks.
|
382
|
+
def each_callback_reverse(&block)
|
383
|
+
fetch_or_create_callbacks.each_reverse(&block)
|
384
|
+
parent.each_callback_reverse(&block)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Excute the callbacks in order. +source+ is the object initiating the event.
|
388
|
+
def execute_callbacks(event)
|
389
|
+
parent.execute_callbacks(event)
|
390
|
+
fetch_or_create_callbacks.each do |callback|
|
391
|
+
callback.call(event)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Callback count including parents
|
396
|
+
def total_callbacks
|
397
|
+
fetch_or_create_callbacks.size + parent.total_callbacks
|
398
|
+
end
|
399
|
+
|
400
|
+
private
|
401
|
+
|
402
|
+
def next_callback_index
|
403
|
+
return 0 if fetch_or_create_callbacks.empty?
|
404
|
+
fetch_or_create_callbacks.map{|cb| cb.index}.max + 1
|
405
|
+
end
|
406
|
+
|
407
|
+
def add_block_callback(type, handle=nil, &block)
|
408
|
+
assert_exists(block)
|
409
|
+
assert(handle.nil? || Symbol === handle)
|
410
|
+
handle ||= next_callback_index
|
411
|
+
add_callback(type.new(handle, block, next_callback_index))
|
412
|
+
end
|
413
|
+
|
414
|
+
def fetch_or_create_callbacks
|
415
|
+
@callbacks ||= CallbackSet.new
|
416
|
+
end
|
417
|
+
|
418
|
+
def disconnect!
|
419
|
+
self.parent = NullHook.new unless root?
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# A null object class for terminating Hook inheritance chains
|
424
|
+
class NullHook
|
425
|
+
def each_callback(&block)
|
426
|
+
# NOOP
|
427
|
+
end
|
428
|
+
|
429
|
+
def each_callback_reverse(&block)
|
430
|
+
# NOOP
|
431
|
+
end
|
432
|
+
|
433
|
+
def execute_callbacks(event)
|
434
|
+
# NOOP
|
435
|
+
end
|
436
|
+
|
437
|
+
def total_callbacks
|
438
|
+
0
|
439
|
+
end
|
440
|
+
|
441
|
+
def terminal?
|
442
|
+
true
|
443
|
+
end
|
444
|
+
|
445
|
+
def root?
|
446
|
+
true
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
class HookSet < Set
|
451
|
+
WILDCARD_HOOK = HookR::Hook.new(:__wildcard__)
|
452
|
+
|
453
|
+
# Find hook by name.
|
454
|
+
#
|
455
|
+
# TODO: Optimize this.
|
456
|
+
def [](key)
|
457
|
+
detect {|v| v.name == key} or raise IndexError, "No such hook: #{key}"
|
458
|
+
end
|
459
|
+
|
460
|
+
def deep_copy
|
461
|
+
result = HookSet.new
|
462
|
+
each do |hook|
|
463
|
+
result << hook.dup
|
464
|
+
end
|
465
|
+
result
|
466
|
+
end
|
467
|
+
|
468
|
+
# Length minus the wildcard hook (if any)
|
469
|
+
def length
|
470
|
+
if include?(WILDCARD_HOOK)
|
471
|
+
super - 1
|
472
|
+
else
|
473
|
+
super
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
class CallbackSet < SortedSet
|
479
|
+
|
480
|
+
# Fetch callback by either index or handle
|
481
|
+
def [](index)
|
482
|
+
case index
|
483
|
+
when Integer then detect{|cb| cb.index == index}
|
484
|
+
when Symbol then detect{|cb| cb.handle == index}
|
485
|
+
else raise ArgumentError, "index must be Integer or Symbol"
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
# get the first callback
|
490
|
+
def first
|
491
|
+
each do |cb|
|
492
|
+
return cb
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def each_reverse(&block)
|
497
|
+
sort{|x, y| y <=> x}.each(&block)
|
498
|
+
end
|
499
|
+
|
500
|
+
end
|
501
|
+
|
502
|
+
Callback = Struct.new(:handle, :index) do
|
503
|
+
include Comparable
|
504
|
+
include FailFast::Assertions
|
505
|
+
|
506
|
+
# Callbacks with the same handle are always equal, which prevents duplicate
|
507
|
+
# handles in CallbackSets. Otherwise, callbacks are sorted by index.
|
508
|
+
def <=>(other)
|
509
|
+
if handle == other.handle
|
510
|
+
return 0
|
511
|
+
end
|
512
|
+
self.index <=> other.index
|
513
|
+
end
|
514
|
+
|
515
|
+
# Must be overridden in subclass
|
516
|
+
def call(*args)
|
517
|
+
raise NotImplementedError, "Callback is an abstract class"
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# A base class for callbacks which execute a block
|
522
|
+
class BlockCallback < Callback
|
523
|
+
attr_reader :block
|
524
|
+
|
525
|
+
def initialize(handle, block, index)
|
526
|
+
@block = block
|
527
|
+
super(handle, index)
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# A callback which will execute outside the event source
|
532
|
+
class ExternalCallback < BlockCallback
|
533
|
+
def call(event)
|
534
|
+
block.call(*event.to_args(block.arity))
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
# A callback which will call a one-arg block with an event object
|
539
|
+
class BasicCallback < BlockCallback
|
540
|
+
def initialize(handle, block, index)
|
541
|
+
check_arity!(block)
|
542
|
+
super
|
543
|
+
end
|
544
|
+
|
545
|
+
def call(event)
|
546
|
+
block.call(event)
|
547
|
+
end
|
548
|
+
|
549
|
+
private
|
550
|
+
|
551
|
+
def check_arity!(block)
|
552
|
+
if block.arity != 1
|
553
|
+
raise ArgumentError, "Callback block must take a single argument"
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
# A callback which will execute in the context of the event source
|
559
|
+
class InternalCallback < BlockCallback
|
560
|
+
def initialize(handle, block, index)
|
561
|
+
assert(block.arity <= 0)
|
562
|
+
super(handle, block, index)
|
563
|
+
end
|
564
|
+
|
565
|
+
def call(event)
|
566
|
+
event.source.instance_eval(&block)
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# A callback which will call a method on the event source
|
571
|
+
class MethodCallback < Callback
|
572
|
+
attr_reader :method
|
573
|
+
|
574
|
+
def initialize(handle, method, index)
|
575
|
+
@method = method
|
576
|
+
super(handle, index)
|
577
|
+
end
|
578
|
+
|
579
|
+
def call(event)
|
580
|
+
method.bind(event.source).call(*event.to_args(method.arity))
|
581
|
+
end
|
582
|
+
end
|
583
|
+
|
584
|
+
# Represents an event which is triggering callbacks.
|
585
|
+
#
|
586
|
+
# +source+:: The object triggering the event.
|
587
|
+
# +name+:: The name of the event
|
588
|
+
# +arguments+:: Any arguments passed associated with the event
|
589
|
+
Event = Struct.new(:source, :name, :arguments, :recursive, :callbacks) do
|
590
|
+
include FailFast::Assertions
|
591
|
+
|
592
|
+
# Convert to arguments for a callback of the given arity. Given an event
|
593
|
+
# with three arguments, the rules are as follows:
|
594
|
+
#
|
595
|
+
# 1. If arity is -1 (meaning any number of arguments), or 4, the result will
|
596
|
+
# be [event, +arguments[0]+, +arguments[1]+, +arguments[2]+]
|
597
|
+
# 2. If arity is 3, the result will just be +arguments+
|
598
|
+
# 3. If arity is < 3, an error will be raised.
|
599
|
+
#
|
600
|
+
# Notice that as the arity is reduced, the event argument is trimmed off.
|
601
|
+
# However, it is not permitted to generate a subset of the +arguments+ list.
|
602
|
+
# If the arity is too small to allow all arguments to be passed, the method
|
603
|
+
# fails.
|
604
|
+
def to_args(arity)
|
605
|
+
case arity
|
606
|
+
when -1
|
607
|
+
full_arguments
|
608
|
+
when (min_argument_count..full_argument_count)
|
609
|
+
full_arguments.slice(full_argument_count - arity, arity)
|
610
|
+
else
|
611
|
+
raise ArgumentError, "Arity must be between #{min_argument_count} "\
|
612
|
+
"and #{full_argument_count}"
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
# This method, along with the callback generator defined in Hook,
|
617
|
+
# implements recursive callback execution.
|
618
|
+
#
|
619
|
+
# TODO: Consider making the next() automatically if the callback doesn't
|
620
|
+
# call it explicitly.
|
621
|
+
#
|
622
|
+
# TODO: Consider adding a cancel() method, implementation TBD.
|
623
|
+
def next(*args)
|
624
|
+
assert(recursive, callbacks)
|
625
|
+
event = self.class.new(source, name, arguments, recursive, callbacks)
|
626
|
+
event.arguments = args unless args.empty?
|
627
|
+
if callbacks.next?
|
628
|
+
callbacks.next.call(event)
|
629
|
+
else
|
630
|
+
raise "No more callbacks!"
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
private
|
635
|
+
|
636
|
+
def full_argument_count
|
637
|
+
full_arguments.size
|
638
|
+
end
|
639
|
+
|
640
|
+
def min_argument_count
|
641
|
+
arguments.size
|
642
|
+
end
|
643
|
+
|
644
|
+
def full_arguments
|
645
|
+
@full_arguments ||= [self, *arguments]
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
end # module HookR
|
650
|
+
|
651
|
+
HookR.require_all_libs_relative_to(__FILE__)
|
652
|
+
|
653
|
+
# EOF
|