aspectory 0.1.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/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
|