aspectory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.@
@@ -0,0 +1,9 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ task :default => [:spec]
4
+
5
+ desc "Run all specs"
6
+ Spec::Rake::SpecTask.new('spec') do |t|
7
+ t.spec_files = FileList['spec/**/*.rb']
8
+ t.spec_opts = ['--colour']
9
+ end
@@ -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