hookr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|