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.
@@ -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