booty-call 0.0.5
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/lib/booty-call.rb +1 -0
- data/lib/booty_call.rb +12 -0
- data/lib/booty_call/callbacker.rb +115 -0
- data/lib/booty_call/hook.rb +51 -0
- data/lib/booty_call/introspector.rb +59 -0
- data/lib/booty_call/observed_method.rb +36 -0
- metadata +68 -0
data/lib/booty-call.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/booty_call.rb'
|
data/lib/booty_call.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
module BootyCall
|
2
|
+
class Callbacker
|
3
|
+
attr_reader :klass
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
@klass = klass
|
7
|
+
extend_klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def before(method_id, *symbols, &block)
|
11
|
+
add_callback(:before, method_id, *symbols, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def after(method_id, *symbols, &block)
|
15
|
+
add_callback(:after, method_id, *symbols, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def around(method_id, *symbols, &block)
|
19
|
+
add_callback(:around, method_id, *symbols, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def extend_klass
|
25
|
+
klass.class_eval do
|
26
|
+
@pristine_cache = Hash.new
|
27
|
+
@callback_cache = { :after => Hash.new([]), :before => Hash.new([]), :around => Hash.new([]) }
|
28
|
+
extend ClassMethods
|
29
|
+
include InstanceMethods
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def add_callback(position, method_id, *symbols, &block)
|
34
|
+
klass.callback_cache[position][method_id] << block if block_given?
|
35
|
+
klass.callback_cache[position][method_id] += symbols
|
36
|
+
klass.callback_cache[position][method_id].compact!
|
37
|
+
klass.callback_cache[position][method_id].uniq!
|
38
|
+
|
39
|
+
klass.pristine_cache[method_id] ||= begin
|
40
|
+
klass.instance_method(method_id).tap do
|
41
|
+
redefine_method method_id
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def redefine_method(method_id)
|
47
|
+
safe_method_id = method_id.to_s
|
48
|
+
safe_method_id.gsub!(/([\w_]+)(\?|!|=|\b)/) { |m| "#{$1}_without_callbacks#{$2}" }
|
49
|
+
klass.class_eval(<<-EOS, "(__DELEGATION__)", 1)
|
50
|
+
def #{method_id}(*args, &block)
|
51
|
+
catch(#{method_id.to_sym.inspect}) do
|
52
|
+
res = nil
|
53
|
+
run_callbacks(:before, #{method_id.inspect})
|
54
|
+
run_callbacks(:around, #{method_id.inspect}) { res = __PRISTINE__(#{method_id.inspect}, *args, &block) }
|
55
|
+
run_callbacks(:after, #{method_id.inspect}, res)
|
56
|
+
res
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def #{safe_method_id}(*args, &block)
|
61
|
+
__PRISTINE__(#{method_id.inspect}, *args, &block)
|
62
|
+
end
|
63
|
+
EOS
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
def pristine_cache
|
68
|
+
@pristine_cache || superclass.pristine_cache
|
69
|
+
end
|
70
|
+
|
71
|
+
def callback_cache
|
72
|
+
@callback_cache || superclass.callback_cache
|
73
|
+
end
|
74
|
+
|
75
|
+
def run_callbacks_for(target, position, method_id, *results, &block)
|
76
|
+
callbacks = callback_cache[position][method_id.to_sym]
|
77
|
+
|
78
|
+
handle = proc do |fn|
|
79
|
+
if fn.is_a?(Proc)
|
80
|
+
block ? proc { instance_exec(block, &fn) } : fn
|
81
|
+
else
|
82
|
+
target.method(fn).arity.abs == results.length ?
|
83
|
+
proc { send(fn, *results, &block) } :
|
84
|
+
proc { send(fn, &block) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
cancel = proc do
|
89
|
+
block ? target.instance_eval(&block) : true
|
90
|
+
end
|
91
|
+
|
92
|
+
callbacks.empty? ? cancel[block] : callbacks.map { |fn|
|
93
|
+
target.instance_exec(*results, &handle[fn])
|
94
|
+
}.all?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module InstanceMethods
|
99
|
+
def run_callbacks(position, method_id, *args, &block)
|
100
|
+
self.class.run_callbacks_for(self, position, method_id, *args, &block).tap do |result|
|
101
|
+
# Halt method propagation if before callbacks return false
|
102
|
+
throw(method_id, false) if position.eql?(:before) and result.eql?(false)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def __PRISTINE__(method_id, *args, &block)
|
107
|
+
if method = self.class.pristine_cache[method_id]
|
108
|
+
method.bind(self).call(*args, &block)
|
109
|
+
else
|
110
|
+
raise NoMethodError, "No method named #{method_id.inspect}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module BootyCall
|
2
|
+
def self.included(klass)
|
3
|
+
klass.send(:include, Hook)
|
4
|
+
end
|
5
|
+
|
6
|
+
module Hook
|
7
|
+
def self.included(klass)
|
8
|
+
klass.class_eval do
|
9
|
+
extend(ClassMethods)
|
10
|
+
@callbacker = BootyCall::Callbacker.new(self)
|
11
|
+
@introspector = BootyCall::Introspector.new(self)
|
12
|
+
@meta_introspector = BootyCall::Introspector.new(self, :meta => true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def before(method_id, *args, &block)
|
18
|
+
callback(:before, method_id, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def after(method_id, *args, &block)
|
22
|
+
callback(:after, method_id, *args, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def around(method_id, *args, &block)
|
26
|
+
callback(:around, method_id, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def observe(method_id, options={}, &block)
|
30
|
+
observer = options[:meta] ? @meta_introspector : @introspector
|
31
|
+
observer.observe(method_id, options, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def callback(position, method_id, *args, &block)
|
35
|
+
case method_id
|
36
|
+
when Regexp
|
37
|
+
return if @introspector.observing?(method_id)
|
38
|
+
observe(method_id, :times => :all) { |m| callback(position, m, *(args << block)) }
|
39
|
+
@introspector.defined_methods \
|
40
|
+
.select { |m| m.to_s =~ method_id } \
|
41
|
+
.reject { |m| @introspector.observing?(m) } \
|
42
|
+
.each { |m| send(position, m, *args, &block) }
|
43
|
+
when Symbol
|
44
|
+
@introspector.has_method?(method_id) ?
|
45
|
+
@callbacker.send(position, method_id, *args, &block) :
|
46
|
+
observe(method_id) { send(position, method_id, *(args << block)) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module BootyCall
|
2
|
+
class Introspector
|
3
|
+
attr_reader :klass, :options
|
4
|
+
|
5
|
+
def initialize(klass, options={})
|
6
|
+
@klass, @options = klass, options
|
7
|
+
@observed_methods = { }
|
8
|
+
end
|
9
|
+
|
10
|
+
def observe(method_id, options={}, &block)
|
11
|
+
observe_klass!
|
12
|
+
@observed_methods[method_id] ||= ObservedMethod.new(method_id, options)
|
13
|
+
@observed_methods[method_id].push(block) if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def observe_klass!
|
17
|
+
@observed ||= begin
|
18
|
+
name = options[:meta] ? :singleton_method_added : :method_added
|
19
|
+
install_hook(name) or true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def defined_methods
|
24
|
+
(klass.instance_methods - Object.instance_methods).map(&:to_sym)
|
25
|
+
end
|
26
|
+
|
27
|
+
def has_method?(method_id)
|
28
|
+
klass.instance_method(method_id) rescue false
|
29
|
+
end
|
30
|
+
|
31
|
+
def observing?(method_id)
|
32
|
+
not not @observed_methods[method_id]
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def install_hook(name)
|
38
|
+
this = self
|
39
|
+
klass.meta_def(name) do |m|
|
40
|
+
this.send :check_method, m
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def check_method(sym)
|
45
|
+
@observed_methods.each do |method_id, observer|
|
46
|
+
without_observers(method_id) do
|
47
|
+
observer.match(sym)
|
48
|
+
observer.valid?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def without_observers(method_id, &block)
|
54
|
+
@observed_methods.delete(method_id).tap do |observer|
|
55
|
+
@observed_methods[method_id] = observer if block[observer]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module BootyCall
|
2
|
+
class ObservedMethod
|
3
|
+
attr_reader :method_id
|
4
|
+
|
5
|
+
def initialize(method_id, options={})
|
6
|
+
@method_id = method_id
|
7
|
+
@handlers = []
|
8
|
+
@count = 0
|
9
|
+
@times = options[:times] || 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def match(sym)
|
13
|
+
return unless case method_id
|
14
|
+
when Symbol then valid? and method_id === sym
|
15
|
+
when Regexp then valid? and method_id.try(:match, sym.to_s)
|
16
|
+
else ; nil
|
17
|
+
end
|
18
|
+
@handlers.each { |fn| fn[sym] }
|
19
|
+
@count += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
if @times.to_s.match(/^(inf|any|all|every)/)
|
24
|
+
def self.valid?; true end
|
25
|
+
true # memoizing the result
|
26
|
+
else
|
27
|
+
@count < @times
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def push(*args)
|
32
|
+
@handlers += args
|
33
|
+
@handlers.tap.compact!.uniq!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: booty-call
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pat Nakajima
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-11-12 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: nakajima-nakajima
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: patnakajima@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/booty_call
|
35
|
+
- lib/booty-call.rb
|
36
|
+
- lib/booty_call.rb
|
37
|
+
- lib/booty_call/hook.rb
|
38
|
+
- lib/booty_call/callbacker.rb
|
39
|
+
- lib/booty_call/introspector.rb
|
40
|
+
- lib/booty_call/observed_method.rb
|
41
|
+
has_rdoc: false
|
42
|
+
homepage: http://github.com/nakajima/booty-call
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.0
|
64
|
+
signing_key:
|
65
|
+
specification_version: 2
|
66
|
+
summary: Callbacks for your Ruby
|
67
|
+
test_files: []
|
68
|
+
|