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