aspectory 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +206 -0
- data/Rakefile +9 -0
- data/lib/aspectory.rb +56 -0
- data/lib/aspectory/callbacker.rb +112 -0
- data/lib/aspectory/hook.rb +51 -0
- data/lib/aspectory/introspector.rb +59 -0
- data/lib/aspectory/observed_method.rb +36 -0
- data/lib/core_ext/method.rb +5 -0
- data/spec/booty_call_spec.rb +7 -0
- data/spec/callbacker_spec.rb +655 -0
- data/spec/hook_spec.rb +233 -0
- data/spec/introspector_spec.rb +280 -0
- data/spec/spec_helper.rb +8 -0
- metadata +67 -0
data/README.textile
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
h1. Aspectory
|
2
|
+
|
3
|
+
h2. Callbacks for Ruby.
|
4
|
+
|
5
|
+
h3. How it works
|
6
|
+
|
7
|
+
Basically, you get three methods: @before@, @after@ and @around@. Each of
|
8
|
+
these takes a method name, then a splat args of symbols and/or a block. The
|
9
|
+
symbols/block will be called before/after the method you specified.
|
10
|
+
|
11
|
+
The @around@ callback gets passed a proc, in the form of an unnamed block
|
12
|
+
for handlers that are methods and a block argument for handlers that are
|
13
|
+
blocks. You must @yield@ or @call@ that block in order for the original
|
14
|
+
method to be called.
|
15
|
+
|
16
|
+
h3. Simple Example
|
17
|
+
|
18
|
+
<pre>
|
19
|
+
require 'rubygems'
|
20
|
+
require 'aspectory'
|
21
|
+
require 'spec'
|
22
|
+
|
23
|
+
class Something
|
24
|
+
include Aspectory::Hook
|
25
|
+
|
26
|
+
attr_reader :results
|
27
|
+
|
28
|
+
around :foo, :round
|
29
|
+
before :foo, :setup
|
30
|
+
after :foo, :teardown
|
31
|
+
|
32
|
+
before :bar do
|
33
|
+
@results << :before
|
34
|
+
end
|
35
|
+
|
36
|
+
around :bar do |fn|
|
37
|
+
@results << :start
|
38
|
+
fn.call
|
39
|
+
@results << :finish
|
40
|
+
end
|
41
|
+
|
42
|
+
after :bar do
|
43
|
+
@results << :after
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@results = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def foo; @results << :foo; :foo end
|
51
|
+
def bar; @results << :bar; :bar end
|
52
|
+
|
53
|
+
def round
|
54
|
+
@results << :start
|
55
|
+
yield
|
56
|
+
@results << :finish
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup
|
60
|
+
@results << :setup
|
61
|
+
end
|
62
|
+
|
63
|
+
def teardown
|
64
|
+
@results << :teardown
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
something = Something.new
|
69
|
+
p something.foo # => :foo
|
70
|
+
p something.results # => [:setup, :start, :foo, :finish, :teardown]
|
71
|
+
|
72
|
+
something = Something.new
|
73
|
+
p something.bar # => :bar
|
74
|
+
p something.results # => [:before, :start, :bar, :finish, :after]
|
75
|
+
</pre>
|
76
|
+
|
77
|
+
<pre>
|
78
|
+
something = Something.new
|
79
|
+
something.foo # => :foo
|
80
|
+
something.results # => [:setup, :foo, :teardown]
|
81
|
+
|
82
|
+
something = Something.new
|
83
|
+
something.bar # => :bar
|
84
|
+
something.results # => [:before, :bar, :after]
|
85
|
+
</pre>
|
86
|
+
|
87
|
+
h3. Calling Methods without Callbacks
|
88
|
+
|
89
|
+
You can use the @#__PRISTINE__@ method to call your methods without any
|
90
|
+
callbacks, or you can just call @method_name_without_callbacks@. Here's an
|
91
|
+
example with the same example class we used above:
|
92
|
+
|
93
|
+
<pre>
|
94
|
+
something = Something.new
|
95
|
+
something.__PRISTINE__(:foo)
|
96
|
+
something.results # => [:foo]
|
97
|
+
something.bar_without_callbacks
|
98
|
+
something.results # => [:foo, :bar]
|
99
|
+
</pre>
|
100
|
+
|
101
|
+
h3. Preventing a method from being called
|
102
|
+
|
103
|
+
If a @before@ callback returns @false@, then the original method will
|
104
|
+
not be called. If you want to halt the method being called, but still
|
105
|
+
want to provide a return value, you can @throw@ the name of the method:
|
106
|
+
|
107
|
+
<pre>
|
108
|
+
class Something
|
109
|
+
before :foo do
|
110
|
+
throw :foo, "from the callback"
|
111
|
+
end
|
112
|
+
|
113
|
+
def foo
|
114
|
+
"from the method"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Something.new.foo # => "from the callback"
|
119
|
+
</pre>
|
120
|
+
|
121
|
+
h3. @after@ callbacks get the results of the method call
|
122
|
+
|
123
|
+
Your @after@ callbacks will be passed whatever the original method
|
124
|
+
call returned:
|
125
|
+
|
126
|
+
<pre>
|
127
|
+
class Something
|
128
|
+
attr_reader :name
|
129
|
+
|
130
|
+
after :foo do |result|
|
131
|
+
@name = result.to_s.capitalize
|
132
|
+
end
|
133
|
+
|
134
|
+
def foo
|
135
|
+
:foo
|
136
|
+
end
|
137
|
+
end
|
138
|
+
</pre>
|
139
|
+
|
140
|
+
<pre>
|
141
|
+
something = Something.new
|
142
|
+
something.name # => nil
|
143
|
+
something.foo # => :foo
|
144
|
+
something.name # => "Foo"
|
145
|
+
</pre>
|
146
|
+
|
147
|
+
h3. Observing method definitions
|
148
|
+
|
149
|
+
If you ever want to see when a method is defined in a class, you can register
|
150
|
+
observers using the @observe@ method. It can take either a symbol or a regular
|
151
|
+
expression, then a callback block will be called when the method is defined. The
|
152
|
+
callback block will be passed the name of the method defined.
|
153
|
+
|
154
|
+
To observe class method definitions, you must pass the @:meta@ option.
|
155
|
+
|
156
|
+
<pre>
|
157
|
+
class Framework
|
158
|
+
include Aspectory
|
159
|
+
|
160
|
+
# Using a symbol
|
161
|
+
observe :admin? do
|
162
|
+
puts "Warning! Overriding the admin method can be dangerous."
|
163
|
+
end
|
164
|
+
|
165
|
+
# Using a regular expression
|
166
|
+
observe(/^_/) do |method_id|
|
167
|
+
puts "Warning! The #{method_id} is not part of the public API!"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Observing a class method definition
|
171
|
+
observe(:find_by_name, :meta => true) do
|
172
|
+
puts "The method :find_by_name already exists in the framework."
|
173
|
+
end
|
174
|
+
|
175
|
+
# Observing multiple occurrences of a method definition
|
176
|
+
observe(/^show_by_/, :times => true) do |method_id|
|
177
|
+
puts "dynamic showing defined: #{method_id}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
</pre>
|
181
|
+
|
182
|
+
h3. Why?
|
183
|
+
|
184
|
+
Why not?
|
185
|
+
|
186
|
+
h3. Requirements
|
187
|
+
|
188
|
+
* "nakajima":http://github.com/nakajima/nakajima @gem install nakajima-nakajima --source=http://gems.github.com@
|
189
|
+
|
190
|
+
h4. TODO
|
191
|
+
|
192
|
+
* Filters (@:if@ and/or @:unless@)
|
193
|
+
* Compilable callbacks ("http://gist.github.com/50397":http://gist.github.com/50397)
|
194
|
+
* Maybe don't worry about @instance_eval@'ing or @instance_exec@'ing callback blocks.
|
195
|
+
* Figure out a way to get it working with metaclasses
|
196
|
+
* Spec suite could definitely be more readable
|
197
|
+
|
198
|
+
h4. Alternatives:
|
199
|
+
|
200
|
+
* http://github.com/sam/extlib/tree/master/lib/extlib/hook.rb
|
201
|
+
* "AspectR":http://aspectr.sourceforge.net
|
202
|
+
* "Aquarium":http://aquarium.rubyforge.org/
|
203
|
+
|
204
|
+
h4. "View the CI build":http://ci.patnakajima.com/booty-call
|
205
|
+
|
206
|
+
@(c) Copyright 2008 Pat Nakajima, released under MIT License.@
|
data/Rakefile
ADDED
data/lib/aspectory.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__) + '/aspectory'
|
2
|
+
$LOAD_PATH << File.dirname(__FILE__) + '/core_ext'
|
3
|
+
|
4
|
+
module BootyCall
|
5
|
+
VERSION = '0.0.5'
|
6
|
+
end
|
7
|
+
|
8
|
+
class Symbol
|
9
|
+
def to_proc
|
10
|
+
Proc.new { |target| target.send(self) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Object
|
15
|
+
def try(sym, *args, &block)
|
16
|
+
respond_to?(sym) ? send(sym, *args, &block) : nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def tap
|
20
|
+
if block_given?
|
21
|
+
yield self
|
22
|
+
self
|
23
|
+
else
|
24
|
+
Class.new {
|
25
|
+
instance_methods.each { |m| undef_method(m) unless m.to_s =~ /__/ }
|
26
|
+
|
27
|
+
def initialize(target)
|
28
|
+
@target = target
|
29
|
+
end
|
30
|
+
|
31
|
+
def method_missing(sym, *args, &block)
|
32
|
+
@target.send(sym, *args, &block)
|
33
|
+
@target
|
34
|
+
end
|
35
|
+
}.new(self)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def metaclass
|
40
|
+
class << self; self end
|
41
|
+
end
|
42
|
+
|
43
|
+
def meta_eval(&block)
|
44
|
+
metaclass.instance_eval(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def meta_def(name, &block)
|
48
|
+
meta_eval { define_method(name, &block) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require 'method'
|
53
|
+
require 'callbacker'
|
54
|
+
require 'observed_method'
|
55
|
+
require 'introspector'
|
56
|
+
require 'hook'
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module Aspectory
|
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
|
+
end
|
75
|
+
|
76
|
+
module InstanceMethods
|
77
|
+
def callbacks_for(position, method_id, *results, &block)
|
78
|
+
callbacks = self.class.callback_cache[position][method_id.to_sym]
|
79
|
+
|
80
|
+
if callbacks.empty?
|
81
|
+
block ? instance_eval(&block) : true
|
82
|
+
else
|
83
|
+
callbacks.map { |callback|
|
84
|
+
if callback.is_a?(Proc)
|
85
|
+
results.unshift(block) if block
|
86
|
+
instance_exec(*results, &callback)
|
87
|
+
else
|
88
|
+
method(callback).arity_match?(results) ?
|
89
|
+
send(callback, *results, &block) :
|
90
|
+
send(callback, &block)
|
91
|
+
end
|
92
|
+
}.all?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def run_callbacks(position, method_id, *args, &block)
|
97
|
+
callbacks_for(position, method_id, *args, &block).tap do |result|
|
98
|
+
# Halt method propagation if before callbacks return false
|
99
|
+
throw(method_id, false) if position.eql?(:before) and result.eql?(false)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def __PRISTINE__(method_id, *args, &block)
|
104
|
+
if method = self.class.pristine_cache[method_id]
|
105
|
+
method.bind(self).call(*args, &block)
|
106
|
+
else
|
107
|
+
raise NoMethodError, "No method named #{method_id.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Aspectory
|
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 = Aspectory::Callbacker.new(self)
|
11
|
+
@introspector = Aspectory::Introspector.new(self)
|
12
|
+
@meta_introspector = Aspectory::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 Aspectory
|
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
|