schleyfox-hookr 1.0.1

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