aquarium 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +53 -0
- data/README +20 -4
- data/Rakefile +19 -4
- data/UPGRADE +41 -1
- data/examples/aspect_design_example.rb +4 -1
- data/examples/aspect_design_example_spec.rb +41 -0
- data/examples/design_by_contract_example.rb +2 -3
- data/examples/design_by_contract_example_spec.rb +92 -0
- data/examples/method_missing_example.rb +1 -1
- data/examples/method_missing_example_spec.rb +59 -0
- data/examples/method_tracing_example.rb +4 -2
- data/examples/method_tracing_example_spec.rb +141 -0
- data/lib/aquarium/aspects/advice.rb +2 -2
- data/lib/aquarium/aspects/aspect.rb +20 -35
- data/lib/aquarium/aspects/dsl.rb +2 -1
- data/lib/aquarium/aspects/dsl/aspect_dsl.rb +12 -8
- data/lib/aquarium/aspects/dsl/object_dsl.rb +8 -0
- data/lib/aquarium/aspects/join_point.rb +16 -10
- data/lib/aquarium/aspects/pointcut.rb +3 -3
- data/lib/aquarium/extras/design_by_contract.rb +20 -11
- data/lib/aquarium/utils.rb +1 -0
- data/lib/aquarium/utils/method_utils.rb +41 -0
- data/lib/aquarium/utils/name_utils.rb +35 -0
- data/lib/aquarium/utils/type_utils.rb +9 -0
- data/lib/aquarium/version.rb +3 -5
- data/rake_tasks/examples.rake +1 -1
- data/spec/aquarium/aspects/aspect_invocation_spec.rb +30 -28
- data/spec/aquarium/aspects/aspect_spec.rb +90 -0
- data/spec/aquarium/aspects/concurrent_aspects_spec.rb +5 -3
- data/spec/aquarium/aspects/concurrent_aspects_with_objects_and_types_spec.rb +3 -1
- data/spec/aquarium/aspects/dsl/aspect_dsl_spec.rb +174 -110
- data/spec/aquarium/aspects/join_point_spec.rb +120 -19
- data/spec/aquarium/aspects/pointcut_spec.rb +24 -14
- data/spec/aquarium/extras/design_by_contract_spec.rb +3 -0
- data/spec/aquarium/finders/finder_result_spec.rb +1 -1
- data/spec/aquarium/finders/method_finder_spec.rb +3 -4
- data/spec/aquarium/spec_example_classes.rb +4 -0
- data/spec/aquarium/utils/method_utils_spec.rb +124 -1
- data/spec/aquarium/utils/name_utils_spec.rb +56 -0
- data/spec/aquarium/utils/type_utils_spec.rb +17 -0
- metadata +12 -4
- data/spec/aquarium/finders/method_sorting_spec.rb +0 -16
data/CHANGES
CHANGED
@@ -1,3 +1,56 @@
|
|
1
|
+
== Version 0.1.5
|
2
|
+
|
3
|
+
Bug fixes:
|
4
|
+
13514 Protected and private methods are made public when advised and left that way when unadvised
|
5
|
+
13650 Loading Aquarium interferes with Rails filters
|
6
|
+
13864 Bug with negative object_id
|
7
|
+
|
8
|
+
Enhancements:
|
9
|
+
13392 Convert examples to specs.
|
10
|
+
13463 Support running in JRuby
|
11
|
+
|
12
|
+
Fixing 13650 required an API change, which is why I've tagged this release "0.1.5" instead of
|
13
|
+
something like "0.1.1" (and the changes don't seem big enough to warrant "0.2.0"...).
|
14
|
+
|
15
|
+
Previously, requiring "aquarium.rb" in the top-level "lib" directory would implicitly require
|
16
|
+
lib/aquarium/aspects/dsl/aspect_dsl.rb, which
|
17
|
+
has Object include the AspectDSL module. This module adds methods like :before and :after to Object.
|
18
|
+
Unfortunately, those methods collide with methods of the same name that Rails adds to Object. It was
|
19
|
+
also a bit presumptuous of me to assume that everyone wanted those methods on Object ;)
|
20
|
+
|
21
|
+
In this release, aspect_dsl.rb is still implicitly included and it still defines the AspectDSL
|
22
|
+
module. Now, however, it does not include the AspectDSL module in Object. Instead, if you want this
|
23
|
+
behavior for all types, you must require the new lib/aquarium/aspects/dsl/object_dsl.rb explicitly.
|
24
|
+
|
25
|
+
As an alternative, if you just want the AspectDSL module included selectively in certain types,
|
26
|
+
then do the following:
|
27
|
+
|
28
|
+
class MyClass # reopen "MyClass"
|
29
|
+
# Add the methods as _class_ methods
|
30
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
31
|
+
end
|
32
|
+
|
33
|
+
or, use (class|module)_eval:
|
34
|
+
|
35
|
+
require 'aquarium/aspects/dsl/aspect_dsl'
|
36
|
+
|
37
|
+
MyClass.class_eval do
|
38
|
+
# Add the methods as _class_ methods
|
39
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
40
|
+
end
|
41
|
+
|
42
|
+
To add the methods as _instance_ methods on individual objects:
|
43
|
+
|
44
|
+
object = MyClass.new
|
45
|
+
object.extend(Aquarium::Aspects::DSL::AspectDSL)
|
46
|
+
|
47
|
+
|
48
|
+
Note: as discussed at http://practicalruby.blogspot.com/2007/02/reopen-with-moduleeval.html,
|
49
|
+
using "class_eval" or "module_eval" is safer that just reopening a class if
|
50
|
+
you're not sure that "MyClass" has actually been defined yet. However, in our particular case, it
|
51
|
+
probably doesn't matter, as AspectDSL doesn't change anything about the type, like aliasing existing
|
52
|
+
methods. Still, we can't guarantee that this won't change in the future.
|
53
|
+
|
1
54
|
== Version 0.1.0
|
2
55
|
|
3
56
|
This is the initial version.
|
data/README
CHANGED
@@ -53,7 +53,7 @@ Many of AspectJ's behaviors that aren't currently supported are planned for futu
|
|
53
53
|
|
54
54
|
Several complete examples are provided in the "examples" directory.
|
55
55
|
|
56
|
-
In most cases, you can either declare the appropriate classes or use the optional DSL, which adds methods to Object.
|
56
|
+
In most cases, you can either declare the appropriate classes or use the optional DSL, which adds methods to classes, objects, or even Object itself.
|
57
57
|
|
58
58
|
Here is an example that traces invocations of all public instance methods of the classes or modules Foo and Bar.
|
59
59
|
|
@@ -64,9 +64,9 @@ Here is an example that traces invocations of all public instance methods of the
|
|
64
64
|
p "Leaving: #{execution_point.type.name}##{execution_point.method_name}"
|
65
65
|
end
|
66
66
|
|
67
|
-
The advice to execute at each join point is the block. The pointcut is the set of all public instance methods in Foo and Bar. (There are additional options available for specifying class methods, protected methods, etc.) Here is the same example using the convenience DSL that adds
|
67
|
+
The advice to execute at each join point is the block. The pointcut is the set of all public instance methods in Foo and Bar. (There are additional options available for specifying class methods, protected methods, etc.) Here is the same example using the convenience DSL that adds aspect methods to Object (available only if you require aquarium/aspects/dsl/object_dsl', since other toolkits, like Rails, define similar methods on Object!).
|
68
68
|
|
69
|
-
require 'aquarium'
|
69
|
+
require 'aquarium/aspects/dsl/object_dsl'
|
70
70
|
around :types => [Foo, Bar], :methods => :all do |execution_point, *args|
|
71
71
|
p "Entering: #{execution_point.type.name}##{execution_point.method_name}"
|
72
72
|
execution_point.proceed
|
@@ -75,9 +75,25 @@ The advice to execute at each join point is the block. The pointcut is the set o
|
|
75
75
|
|
76
76
|
See "examples/method_tracing_example.rb" for a more detailed version of this example.
|
77
77
|
|
78
|
+
If you don't want to add these methods to Object, you can also add them individually to modules, classes, or objects:
|
79
|
+
|
80
|
+
require 'aquarium/aspects/dsl/aspect_dsl'
|
81
|
+
...
|
82
|
+
module MyModule
|
83
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
84
|
+
end
|
85
|
+
|
86
|
+
class MyClass
|
87
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
88
|
+
end
|
89
|
+
|
90
|
+
my_object = MyOtherClass.new
|
91
|
+
my_object.extend (Aquarium::Aspects::DSL::AspectDSL)
|
92
|
+
|
78
93
|
If you use the DSL inside a class and omit the :type(s) and :object(s) options, "self" is assumed.
|
79
94
|
|
80
95
|
class Foo
|
96
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
81
97
|
...
|
82
98
|
def critical_operation *args
|
83
99
|
...
|
@@ -92,7 +108,7 @@ If you use the DSL inside a class and omit the :type(s) and :object(s) options,
|
|
92
108
|
end
|
93
109
|
end
|
94
110
|
|
95
|
-
Here are some more succinct examples, illustrating the API.
|
111
|
+
Here are some more succinct examples, illustrating the API (using the DSL methods).
|
96
112
|
|
97
113
|
You can pass in pointcuts defined elsewhere:
|
98
114
|
|
data/Rakefile
CHANGED
@@ -31,7 +31,7 @@ task :default => [:verify_rcov]
|
|
31
31
|
|
32
32
|
desc "Run all specs"
|
33
33
|
Spec::Rake::SpecTask.new do |t|
|
34
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
t.spec_files = FileList['spec/**/*_spec.rb', 'examples/**/*_spec.rb']
|
35
35
|
t.spec_opts = ['--options', 'spec.opts']
|
36
36
|
t.rcov = true
|
37
37
|
t.rcov_dir = '../doc/output/coverage'
|
@@ -40,7 +40,7 @@ end
|
|
40
40
|
|
41
41
|
desc "Run all specs and store html output in doc/output/report.html"
|
42
42
|
Spec::Rake::SpecTask.new('spec_html') do |t|
|
43
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
43
|
+
t.spec_files = FileList['spec/**/*_spec.rb', 'examples/**/*_spec.rb']
|
44
44
|
t.spec_opts = ['--format html:../doc/output/report.html','--backtrace']
|
45
45
|
end
|
46
46
|
|
@@ -168,8 +168,10 @@ end
|
|
168
168
|
desc "Upload Website to RubyForge"
|
169
169
|
task :publish_website => [:verify_user, :website] do
|
170
170
|
unless Aquarium::VERSION::RELEASE_CANDIDATE
|
171
|
+
# host = "aquarium-website@rubyforge.org"
|
172
|
+
host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
|
171
173
|
publisher = Rake::SshDirPublisher.new(
|
172
|
-
"
|
174
|
+
"#{host}",
|
173
175
|
"/var/www/gforge-projects/#{PKG_NAME}",
|
174
176
|
"../doc/output"
|
175
177
|
)
|
@@ -179,6 +181,18 @@ task :publish_website => [:verify_user, :website] do
|
|
179
181
|
end
|
180
182
|
end
|
181
183
|
|
184
|
+
desc "Upload Website archive to RubyForge"
|
185
|
+
task :archive_website => [:verify_user, :website] do
|
186
|
+
# host = "aquarium-website@rubyforge.org"
|
187
|
+
host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
|
188
|
+
publisher = Rake::SshDirPublisher.new(
|
189
|
+
"#{host}",
|
190
|
+
"/var/www/gforge-projects/#{PKG_NAME}/#{Spec::VERSION::TAG}",
|
191
|
+
"../doc/output"
|
192
|
+
)
|
193
|
+
publisher.upload
|
194
|
+
end
|
195
|
+
|
182
196
|
desc "Publish gem+tgz+zip on RubyForge. You must make sure lib/version.rb is aligned with the CHANGELOG file"
|
183
197
|
task :publish_packages => [:verify_user, :package] do
|
184
198
|
release_files = FileList[
|
@@ -200,7 +214,8 @@ task :publish_packages => [:verify_user, :package] do
|
|
200
214
|
puts "SINCE THIS IS A PRERELEASE, FILES ARE UPLOADED WITH SSH, NOT TO THE RUBYFORGE FILE SECTION"
|
201
215
|
puts "YOU MUST TYPE THE PASSWORD #{release_files.length} TIMES..."
|
202
216
|
|
203
|
-
host = "aquarium-website@rubyforge.org"
|
217
|
+
# host = "aquarium-website@rubyforge.org"
|
218
|
+
host = "#{ENV['RUBYFORGE_USER']}@rubyforge.org"
|
204
219
|
remote_dir = "/var/www/gforge-projects/#{PKG_NAME}"
|
205
220
|
|
206
221
|
publisher = Rake::SshFilePublisher.new(
|
data/UPGRADE
CHANGED
@@ -1,3 +1,43 @@
|
|
1
|
-
|
1
|
+
== Upgrading existing code to Aquarium-0.1.5
|
2
|
+
|
3
|
+
This is mostly a bug-fix release, but it did have to introduce one API change, as described in the
|
4
|
+
CHANGES. In particular, the aspect "DSL" methods are no longer automatically to Object, as some of
|
5
|
+
their names overlap with methods added by Rails.
|
6
|
+
|
7
|
+
Now, if you want these methods added to Object, you must require the new
|
8
|
+
lib/aquarium/aspects/dsl/object_dsl.rb explicitly.
|
9
|
+
|
10
|
+
As an alternative, if you just want these methods added selectively in certain types, then do the
|
11
|
+
following:
|
12
|
+
|
13
|
+
<ruby>
|
14
|
+
require 'aquarium/aspects/dsl/aspect_dsl'
|
15
|
+
|
16
|
+
class MyClass # reopen "MyClass"
|
17
|
+
# Add the methods as _class_ methods
|
18
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
19
|
+
end
|
20
|
+
</ruby>
|
21
|
+
|
22
|
+
or, use (class|module)_eval:
|
23
|
+
<ruby>
|
24
|
+
require 'aquarium/aspects/dsl/aspect_dsl'
|
25
|
+
|
26
|
+
MyClass.class_eval do
|
27
|
+
# Add the methods as _class_ methods
|
28
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
29
|
+
end
|
30
|
+
</ruby>
|
31
|
+
|
32
|
+
To add the methods as _instance_ methods on individual objects:
|
33
|
+
|
34
|
+
<ruby>
|
35
|
+
object = MyClass.new
|
36
|
+
object.extend(Aquarium::Aspects::DSL::AspectDSL)
|
37
|
+
</ruby>
|
38
|
+
|
39
|
+
See the CHANGES for more details.
|
40
|
+
|
41
|
+
== Upgrading existing code to Aquarium-0.1.0
|
2
42
|
|
3
43
|
This is the first release of Aquarium.
|
@@ -12,6 +12,7 @@ require 'aquarium'
|
|
12
12
|
|
13
13
|
module Aquarium
|
14
14
|
class ClassWithStateAndBehavior
|
15
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
15
16
|
def initialize *args
|
16
17
|
@state = args
|
17
18
|
p "Initializing: #{args.inspect}"
|
@@ -24,9 +25,11 @@ module Aquarium
|
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
28
|
+
include Aquarium::Aspects
|
29
|
+
|
27
30
|
# Observe state changes in the class, using the class-defined pointcut.
|
28
31
|
|
29
|
-
observer = after :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
|
32
|
+
observer = Aspect.new :after, :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
|
30
33
|
p "State has changed. "
|
31
34
|
p " New state is #{jp.context.advised_object.state.inspect}"
|
32
35
|
p " Equivalent to *args: #{args.inspect}"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
|
2
|
+
require 'aquarium'
|
3
|
+
|
4
|
+
# Example demonstrating emerging ideas about good aspect-oriented design. Specifically, this
|
5
|
+
# example follows ideas of Jonathan Aldrich on "Open Modules", where a "module" (in the generic
|
6
|
+
# sense of the word...) is responsible for defining and maintaining the pointcuts that it is
|
7
|
+
# willing to expose to potential aspects. Aspects are only allowed to advise the module through
|
8
|
+
# the pointcut. (Enforcing this constraint is TBD)
|
9
|
+
# Griswold, Sullivan, and collaborators have expanded on these ideas. See their IEEE Software,
|
10
|
+
# March 2006 paper.
|
11
|
+
|
12
|
+
module Aquarium
|
13
|
+
class ClassWithStateAndBehavior
|
14
|
+
include Aquarium::Aspects::DSL::AspectDSL
|
15
|
+
def initialize *args
|
16
|
+
@state = args
|
17
|
+
end
|
18
|
+
attr_accessor :state
|
19
|
+
|
20
|
+
# A simpler version of the following would be
|
21
|
+
# STATE_CHANGE = pointcut :method => :state
|
22
|
+
STATE_CHANGE = pointcut :attribute => :state, :attribute_options => :writer
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
include Aquarium::Aspects
|
27
|
+
|
28
|
+
describe "An example of an aspect using a class-defined pointcut." do
|
29
|
+
it "should observe state changes in the class." do
|
30
|
+
@new_state = nil
|
31
|
+
observer = Aspect.new :after, :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
|
32
|
+
@new_state = jp.context.advised_object.state
|
33
|
+
@new_state.should be_eql(*args)
|
34
|
+
end
|
35
|
+
object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
|
36
|
+
object.state = [:b1, :b2]
|
37
|
+
@new_state.should == [:b1, :b2]
|
38
|
+
observer.unadvise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -3,9 +3,8 @@
|
|
3
3
|
# specifying the contract of use for a class or module and testing it at runtime (usually
|
4
4
|
# during the testing process)
|
5
5
|
# This example is adapted from spec/extras/design_by_contract_spec.rb.
|
6
|
-
# Note: the DesignByContract module
|
7
|
-
#
|
8
|
-
# "self" as the :object to advise.
|
6
|
+
# Note: the DesignByContract module adds the #precondition, #postcondition, and #invariant
|
7
|
+
# methods shown below to Object and they use "self" as the :object to advise.
|
9
8
|
|
10
9
|
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
11
10
|
require 'aquarium/extras/design_by_contract'
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
|
2
|
+
require 'aquarium'
|
3
|
+
require 'aquarium/extras/design_by_contract'
|
4
|
+
|
5
|
+
# Example demonstrating "Design by Contract", Bertrand Meyer's idea for programmatically-
|
6
|
+
# specifying the contract of use for a class or module and testing it at runtime (usually
|
7
|
+
# during the testing process)
|
8
|
+
# This example is adapted from spec/extras/design_by_contract_spec.rb.
|
9
|
+
# Note: the DesignByContract module adds the #precondition, #postcondition, and #invariant
|
10
|
+
# methods shown below to Object and they use "self" as the :object to advise.
|
11
|
+
|
12
|
+
module Aquarium
|
13
|
+
class PreCondExample
|
14
|
+
def action *args
|
15
|
+
@state = *args
|
16
|
+
end
|
17
|
+
attr_reader :state
|
18
|
+
|
19
|
+
precondition :method => :action, :message => "Must pass more than one argument." do |jp, *args|
|
20
|
+
args.size > 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "An example using a precondition" do
|
26
|
+
it "should fail at the call entry point if the precondition is not satisfied." do
|
27
|
+
lambda { Aquarium::PreCondExample.new.action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "An example using a precondition" do
|
32
|
+
it "should not fail at the call entry point if the precondition is satisfied." do
|
33
|
+
Aquarium::PreCondExample.new.action :a1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module Aquarium
|
38
|
+
class PostCondExample
|
39
|
+
def action *args
|
40
|
+
@state = *args
|
41
|
+
end
|
42
|
+
attr_reader :state
|
43
|
+
|
44
|
+
postcondition :method => :action,
|
45
|
+
:message => "Must pass more than one argument and first argument must be non-empty." do |jp, *args|
|
46
|
+
args.size > 0 && ! args[0].empty?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "An example using a postcondition" do
|
52
|
+
it "should fail at the call exit point if the postcondition is not satisfied." do
|
53
|
+
lambda { Aquarium::PostCondExample.new.action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
|
54
|
+
lambda { Aquarium::PostCondExample.new.action "" }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "An example using a postcondition" do
|
59
|
+
it "should not fail at the call entry point if the postcondition is satisfied." do
|
60
|
+
Aquarium::PostCondExample.new.action :a1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Aquarium
|
65
|
+
class InvarCondExample
|
66
|
+
def initialize
|
67
|
+
@invar = 0
|
68
|
+
end
|
69
|
+
attr_reader :invar
|
70
|
+
def good_action
|
71
|
+
end
|
72
|
+
def bad_action
|
73
|
+
@invar = 1
|
74
|
+
end
|
75
|
+
|
76
|
+
invariant :methods => /action$/, :message => "Must not change the @invar value." do |jp, *args|
|
77
|
+
jp.context.advised_object.invar == 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "An example using an invariant" do
|
83
|
+
it "should fail at the call entry or exit point if the invariant is not satisfied." do
|
84
|
+
lambda { Aquarium::InvarCondExample.new.bad_action }.should raise_error(Aquarium::Extras::DesignByContract::ContractError)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "An example using an invariant" do
|
89
|
+
it "should pass at the call entry and exit point if the invariant is satisfied." do
|
90
|
+
Aquarium::InvarCondExample.new.good_action
|
91
|
+
end
|
92
|
+
end
|
@@ -28,7 +28,7 @@ echo1.say "hello", "world!"
|
|
28
28
|
echo1.log "something", "interesting..."
|
29
29
|
echo1.shout "theater", "in", "a", "crowded", "firehouse!"
|
30
30
|
|
31
|
-
around :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
|
31
|
+
Aquarium::Aspects::Aspect.new :around, :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
|
32
32
|
if sym == :log
|
33
33
|
p "--- Sending to log: #{args.join(" ")}"
|
34
34
|
else
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec/aquarium/spec_helper.rb'
|
2
|
+
require 'aquarium'
|
3
|
+
|
4
|
+
# Example demonstrating "around" advice for method_missing. This is a technique for
|
5
|
+
# avoiding collisions when different toolkits want to override method_missing in the
|
6
|
+
# same classes, e.g., Object. Using around advice as shown allows a toolkit to add
|
7
|
+
# custom behavior while invoking the "native" method_missing to handle unrecognized
|
8
|
+
# method calls.
|
9
|
+
# Note that it is essential to use around advice, not before or after advice, because
|
10
|
+
# neither can prevent the call to the "wrapped" method_missing, which is presumably
|
11
|
+
# not what you want.
|
12
|
+
# In this (contrived) example, an Echo class uses method_missing to simply echo
|
13
|
+
# the method name and arguments. An aspect is used to intercept any calls to a
|
14
|
+
# fictitious "log" method and handle those in a different way.
|
15
|
+
|
16
|
+
module Aquarium
|
17
|
+
class Echo
|
18
|
+
def method_missing sym, *args
|
19
|
+
@log ||= []
|
20
|
+
@log << "Echoing: #{sym.to_s}: #{args.join(" ")}"
|
21
|
+
end
|
22
|
+
def logged_messages; @log; end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "An example of a class' method_missing without around advice" do
|
27
|
+
it "should handle all invocations of method_missing." do
|
28
|
+
echo = Aquarium::Echo.new
|
29
|
+
echo.say "hello", "world!"
|
30
|
+
echo.log "something", "interesting..."
|
31
|
+
echo.shout "theater", "in", "a", "crowded", "firehouse!"
|
32
|
+
echo.logged_messages.size.should == 3
|
33
|
+
echo.logged_messages[0].should == "Echoing: say: hello world!"
|
34
|
+
echo.logged_messages[1].should == "Echoing: log: something interesting..."
|
35
|
+
echo.logged_messages[2].should == "Echoing: shout: theater in a crowded firehouse!"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "An example of a class' method_missing with around advice" do
|
40
|
+
it "should only handle invocations not processed by the around advice." do
|
41
|
+
@intercepted_message = nil
|
42
|
+
aspect = Aquarium::Aspects::Aspect.new :around, :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
|
43
|
+
if sym == :log
|
44
|
+
@intercepted_message = "log: #{args.join(" ")}"
|
45
|
+
else
|
46
|
+
join_point.proceed
|
47
|
+
end
|
48
|
+
end
|
49
|
+
echo = Aquarium::Echo.new
|
50
|
+
echo.say "hello", "world!"
|
51
|
+
echo.log "something", "interesting..."
|
52
|
+
echo.shout "theater", "in", "a", "crowded", "firehouse!"
|
53
|
+
echo.logged_messages.size.should == 2
|
54
|
+
echo.logged_messages[0].should == "Echoing: say: hello world!"
|
55
|
+
echo.logged_messages[1].should == "Echoing: shout: theater in a crowded firehouse!"
|
56
|
+
@intercepted_message.should == "log: something interesting..."
|
57
|
+
aspect.unadvise
|
58
|
+
end
|
59
|
+
end
|