hookr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(
4
+ File.join(File.dirname(__FILE__), %w[.. lib hookr]))
5
+
6
+ # Put your code here
7
+
8
+ # EOF
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