aquarium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/CHANGES +4 -0
  2. data/EXAMPLES.rd +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README +250 -0
  5. data/RELEASE-PLAN +1 -0
  6. data/Rakefile +236 -0
  7. data/UPGRADE +3 -0
  8. data/examples/aspect_design_example.rb +36 -0
  9. data/examples/design_by_contract_example.rb +88 -0
  10. data/examples/method_missing_example.rb +44 -0
  11. data/examples/method_tracing_example.rb +64 -0
  12. data/lib/aquarium.rb +7 -0
  13. data/lib/aquarium/aspects.rb +6 -0
  14. data/lib/aquarium/aspects/advice.rb +189 -0
  15. data/lib/aquarium/aspects/aspect.rb +577 -0
  16. data/lib/aquarium/aspects/default_object_handler.rb +27 -0
  17. data/lib/aquarium/aspects/dsl.rb +1 -0
  18. data/lib/aquarium/aspects/dsl/aspect_dsl.rb +61 -0
  19. data/lib/aquarium/aspects/join_point.rb +158 -0
  20. data/lib/aquarium/aspects/pointcut.rb +254 -0
  21. data/lib/aquarium/aspects/pointcut_composition.rb +36 -0
  22. data/lib/aquarium/extensions.rb +5 -0
  23. data/lib/aquarium/extensions/hash.rb +85 -0
  24. data/lib/aquarium/extensions/regexp.rb +20 -0
  25. data/lib/aquarium/extensions/set.rb +49 -0
  26. data/lib/aquarium/extensions/string.rb +13 -0
  27. data/lib/aquarium/extensions/symbol.rb +22 -0
  28. data/lib/aquarium/extras.rb +4 -0
  29. data/lib/aquarium/extras/design_by_contract.rb +64 -0
  30. data/lib/aquarium/finders.rb +4 -0
  31. data/lib/aquarium/finders/finder_result.rb +121 -0
  32. data/lib/aquarium/finders/method_finder.rb +228 -0
  33. data/lib/aquarium/finders/object_finder.rb +74 -0
  34. data/lib/aquarium/finders/type_finder.rb +127 -0
  35. data/lib/aquarium/utils.rb +9 -0
  36. data/lib/aquarium/utils/array_utils.rb +29 -0
  37. data/lib/aquarium/utils/hash_utils.rb +28 -0
  38. data/lib/aquarium/utils/html_escaper.rb +17 -0
  39. data/lib/aquarium/utils/invalid_options.rb +9 -0
  40. data/lib/aquarium/utils/method_utils.rb +18 -0
  41. data/lib/aquarium/utils/nil_object.rb +13 -0
  42. data/lib/aquarium/utils/set_utils.rb +32 -0
  43. data/lib/aquarium/version.rb +30 -0
  44. data/rake_tasks/examples.rake +7 -0
  45. data/rake_tasks/examples_specdoc.rake +8 -0
  46. data/rake_tasks/examples_with_rcov.rake +8 -0
  47. data/rake_tasks/verify_rcov.rake +7 -0
  48. data/spec/aquarium/aspects/advice_chain_node_spec.rb +34 -0
  49. data/spec/aquarium/aspects/advice_spec.rb +103 -0
  50. data/spec/aquarium/aspects/aspect_invocation_spec.rb +111 -0
  51. data/spec/aquarium/aspects/aspect_spec.rb +978 -0
  52. data/spec/aquarium/aspects/aspect_with_nested_types_spec.rb +129 -0
  53. data/spec/aquarium/aspects/concurrent_aspects_spec.rb +423 -0
  54. data/spec/aquarium/aspects/concurrent_aspects_with_objects_and_types_spec.rb +103 -0
  55. data/spec/aquarium/aspects/concurrently_accessed.rb +21 -0
  56. data/spec/aquarium/aspects/dsl/aspect_dsl_spec.rb +514 -0
  57. data/spec/aquarium/aspects/join_point_spec.rb +302 -0
  58. data/spec/aquarium/aspects/pointcut_and_composition_spec.rb +131 -0
  59. data/spec/aquarium/aspects/pointcut_or_composition_spec.rb +111 -0
  60. data/spec/aquarium/aspects/pointcut_spec.rb +800 -0
  61. data/spec/aquarium/extensions/hash_spec.rb +187 -0
  62. data/spec/aquarium/extensions/regex_spec.rb +40 -0
  63. data/spec/aquarium/extensions/set_spec.rb +105 -0
  64. data/spec/aquarium/extensions/string_spec.rb +25 -0
  65. data/spec/aquarium/extensions/symbol_spec.rb +37 -0
  66. data/spec/aquarium/extras/design_by_contract_spec.rb +68 -0
  67. data/spec/aquarium/finders/finder_result_spec.rb +359 -0
  68. data/spec/aquarium/finders/method_finder_spec.rb +878 -0
  69. data/spec/aquarium/finders/method_sorting_spec.rb +16 -0
  70. data/spec/aquarium/finders/object_finder_spec.rb +230 -0
  71. data/spec/aquarium/finders/type_finder_spec.rb +210 -0
  72. data/spec/aquarium/spec_example_classes.rb +117 -0
  73. data/spec/aquarium/spec_helper.rb +3 -0
  74. data/spec/aquarium/utils/array_utils_spec.rb +47 -0
  75. data/spec/aquarium/utils/hash_utils_spec.rb +48 -0
  76. data/spec/aquarium/utils/html_escaper_spec.rb +18 -0
  77. data/spec/aquarium/utils/method_utils_spec.rb +50 -0
  78. data/spec/aquarium/utils/nil_object_spec.rb +19 -0
  79. data/spec/aquarium/utils/set_utils_spec.rb +60 -0
  80. metadata +132 -0
data/UPGRADE ADDED
@@ -0,0 +1,3 @@
1
+ = Upgrading existing code to Aquarium-0.1.0
2
+
3
+ This is the first release of Aquarium.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # Example demonstrating emerging ideas about good aspect-oriented design. Specifically, this
3
+ # example follows ideas of Jonathan Aldrich on "Open Modules", where a "module" (in the generic
4
+ # sense of the word...) is responsible for defining and maintaining the pointcuts that it is
5
+ # willing to expose to potential aspects. Aspects are only allowed to advise the module through
6
+ # the pointcut. (Enforcing this constraint is TBD)
7
+ # Griswold, Sullivan, and collaborators have expanded on these ideas. See their IEEE Software,
8
+ # March 2006 paper.
9
+
10
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
11
+ require 'aquarium'
12
+
13
+ module Aquarium
14
+ class ClassWithStateAndBehavior
15
+ def initialize *args
16
+ @state = args
17
+ p "Initializing: #{args.inspect}"
18
+ end
19
+ attr_accessor :state
20
+
21
+ # A simpler version of the following would be
22
+ # STATE_CHANGE = pointcut :method => :state
23
+ STATE_CHANGE = pointcut :attribute => :state, :attribute_options => :writer
24
+ end
25
+ end
26
+
27
+ # Observe state changes in the class, using the class-defined pointcut.
28
+
29
+ observer = after :pointcut => Aquarium::ClassWithStateAndBehavior::STATE_CHANGE do |jp, *args|
30
+ p "State has changed. "
31
+ p " New state is #{jp.context.advised_object.state.inspect}"
32
+ p " Equivalent to *args: #{args.inspect}"
33
+ end
34
+
35
+ object = Aquarium::ClassWithStateAndBehavior.new(:a1, :a2, :a3)
36
+ object.state = [:b1, :b2]
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env ruby
2
+ # Example demonstrating "Design by Contract", Bertrand Meyer's idea for programmatically-
3
+ # specifying the contract of use for a class or module and testing it at runtime (usually
4
+ # during the testing process)
5
+ # This example is adapted from spec/extras/design_by_contract_spec.rb.
6
+ # Note: the DesignByContract module uses the AspectDSL module. The #precondition, #postcondition,
7
+ # and #invariant methods shown below delegate to AspectDSL methods. Those methods implicitly use
8
+ # "self" as the :object to advise.
9
+
10
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
11
+ require 'aquarium/extras/design_by_contract'
12
+
13
+ module Aquarium
14
+ class PreCond
15
+ def action *args
16
+ p "inside :action"
17
+ end
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
+ p "This call will fail because the precondition is not satisfied:"
26
+ begin
27
+ Aquarium::PreCond.new.action
28
+ rescue Aquarium::Extras::DesignByContract::ContractError => e
29
+ p e.inspect
30
+ end
31
+ p "This call will pass because the precondition is satisfied:"
32
+ Aquarium::PreCond.new.action :a1
33
+
34
+ module Aquarium
35
+ class PostCond
36
+ def action *args
37
+ p "inside :action"
38
+ end
39
+
40
+ postcondition :method => :action,
41
+ :message => "Must pass more than one argument and first argument must be non-empty." do |jp, *args|
42
+ args.size > 0 && ! args[0].empty?
43
+ end
44
+ end
45
+ end
46
+
47
+ p "These two calls will fail because the postcondition is not satisfied:"
48
+ begin
49
+ Aquarium::PostCond.new.action
50
+ rescue Aquarium::Extras::DesignByContract::ContractError => e
51
+ p e.inspect
52
+ end
53
+ begin
54
+ Aquarium::PostCond.new.action ""
55
+ rescue Aquarium::Extras::DesignByContract::ContractError => e
56
+ p e.inspect
57
+ end
58
+ p "This call will pass because the postcondition is satisfied:"
59
+ Aquarium::PostCond.new.action :a1
60
+
61
+ module Aquarium
62
+ class InvarCond
63
+ def initialize
64
+ @invar = 0
65
+ end
66
+ attr_reader :invar
67
+ def good_action
68
+ p "inside :good_action"
69
+ end
70
+ def bad_action
71
+ p "inside :bad_action"
72
+ @invar = 1
73
+ end
74
+
75
+ invariant :methods => /action$/, :message => "Must not change the @invar value." do |jp, *args|
76
+ jp.context.advised_object.invar == 0
77
+ end
78
+ end
79
+ end
80
+
81
+ p "This call will fail because the invariant is not satisfied:"
82
+ begin
83
+ Aquarium::InvarCond.new.bad_action
84
+ rescue Aquarium::Extras::DesignByContract::ContractError => e
85
+ p e.inspect
86
+ end
87
+ p "This call will pass because the invariant is satisfied:"
88
+ Aquarium::InvarCond.new.good_action
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # Example demonstrating "around" advice for method_missing. This is a technique for
3
+ # avoiding collisions when different toolkits want to override method_missing in the
4
+ # same classes, e.g., Object. Using around advice as shown allows a toolkit to add
5
+ # custom behavior while invoking the "native" method_missing to handle unrecognized
6
+ # method calls.
7
+ # Note that it is essential to use around advice, not before or after advice, because
8
+ # neither can prevent the call to the "wrapped" method_missing, which is presumably
9
+ # not what you want.
10
+ # In this (contrived) example, an Echo class uses method_missing to simply echo
11
+ # the method name and arguments. An aspect is used to intercept any calls to a
12
+ # fictitious "log" method and handle those in a different way.
13
+
14
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
15
+ require 'aquarium'
16
+
17
+ module Aquarium
18
+ class Echo
19
+ def method_missing sym, *args
20
+ p "Echoing: #{sym.to_s}: #{args.join(" ")}"
21
+ end
22
+ end
23
+ end
24
+
25
+ p "Before advising Echo:"
26
+ echo1 = Aquarium::Echo.new
27
+ echo1.say "hello", "world!"
28
+ echo1.log "something", "interesting..."
29
+ echo1.shout "theater", "in", "a", "crowded", "firehouse!"
30
+
31
+ around :type => Aquarium::Echo, :method => :method_missing do |join_point, sym, *args|
32
+ if sym == :log
33
+ p "--- Sending to log: #{args.join(" ")}"
34
+ else
35
+ join_point.proceed
36
+ end
37
+ end
38
+
39
+ p "After advising Echo:"
40
+ echo2 = Aquarium::Echo.new
41
+ echo2.say "hello", "world!"
42
+ echo2.log "something", "interesting..."
43
+ echo2.shout "theater", "in", "a", "crowded", "firehouse!"
44
+
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env ruby
2
+ # Example demonstrating "around" advice that traces calls to all methods in
3
+ # classes Foo and Bar
4
+
5
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
6
+ require 'aquarium'
7
+
8
+ module Aquarium
9
+ class Foo
10
+ def initialize *args
11
+ p "Inside: Foo#initialize: args = #{args.inspect}"
12
+ end
13
+ def do_it *args
14
+ p "Inside: Foo#do_it: args = #{args.inspect}"
15
+ end
16
+ end
17
+
18
+ module BarModule
19
+ def initialize *args
20
+ p "Inside: BarModule#initialize: args = #{args.inspect}"
21
+ end
22
+ def do_something_else *args
23
+ p "Inside: BarModule#do_something_else: args = #{args.inspect}"
24
+ end
25
+ end
26
+
27
+ class Bar
28
+ include BarModule
29
+ end
30
+ end
31
+
32
+ p "Before advising the methods:"
33
+ foo1 = Aquarium::Foo.new :a1, :a2
34
+ foo1.do_it :b1, :b2
35
+
36
+ bar1 = Aquarium::Bar.new :a3, :a4
37
+ bar1.do_something_else :b3, :b4
38
+
39
+ around :types => [Aquarium::Foo, Aquarium::Bar], :methods => :all, :method_options => :suppress_ancestor_methods do |execution_point, *args|
40
+ p "Entering: #{execution_point.type.name}##{execution_point.method_name}: args = #{args.inspect}"
41
+ execution_point.proceed
42
+ p "Leaving: #{execution_point.type.name}##{execution_point.method_name}: args = #{args.inspect}"
43
+ end
44
+
45
+ p "After advising the methods. Notice that #intialize isn't advised:"
46
+ foo2 = Aquarium::Foo.new :a5, :a6
47
+ foo2.do_it :b5, :b6
48
+
49
+ bar1 = Aquarium::Bar.new :a7, :a8
50
+ bar1.do_something_else :b7, :b8
51
+
52
+ around :types => [Aquarium::Foo, Aquarium::Bar], :methods => :initialize, :method_options => :private do |execution_point, *args|
53
+ p "Entering: #{execution_point.type.name}##{execution_point.method_name}: args = #{args.inspect}"
54
+ execution_point.proceed
55
+ p "Leaving: #{execution_point.type.name}##{execution_point.method_name}: args = #{args.inspect}"
56
+ end
57
+
58
+ p "After advising the private methods. Notice that #intialize is advised:"
59
+ foo2 = Aquarium::Foo.new :a9, :a10
60
+ foo2.do_it :b9, :b10
61
+
62
+ bar1 = Aquarium::Bar.new :a11, :a12
63
+ bar1.do_something_else :b11, :b12
64
+
data/lib/aquarium.rb ADDED
@@ -0,0 +1,7 @@
1
+ # Does not include 'aquarium/extras'. Users have to include it explicitly if they want it.
2
+ require 'aquarium/utils'
3
+ require 'aquarium/extensions'
4
+ require 'aquarium/finders'
5
+ require 'aquarium/aspects'
6
+ require 'aquarium/version'
7
+
@@ -0,0 +1,6 @@
1
+ require 'aquarium/aspects/advice'
2
+ require 'aquarium/aspects/aspect'
3
+ require 'aquarium/aspects/join_point'
4
+ require 'aquarium/aspects/pointcut'
5
+ require 'aquarium/aspects/pointcut_composition'
6
+ require 'aquarium/aspects/dsl'
@@ -0,0 +1,189 @@
1
+ require 'aquarium/utils/array_utils'
2
+ require 'aquarium/extensions/string'
3
+ require 'aquarium/extensions/symbol'
4
+ require 'aquarium/utils/invalid_options'
5
+ require 'aquarium/utils/nil_object'
6
+
7
+ module Aquarium
8
+ module Aspects
9
+ module Advice
10
+ def self.kinds_in_priority_order
11
+ [:around, :before, :after, :after_returning, :after_raising]
12
+ end
13
+
14
+ def self.kinds; self.kinds_in_priority_order; end
15
+
16
+ def self.sort_by_priority_order advice_kinds
17
+ advice_kinds.sort do |x,y|
18
+ self.kinds_in_priority_order.index(x.to_sym) <=> self.kinds_in_priority_order.index(y.to_sym)
19
+ end.map {|x| x.to_sym}
20
+ end
21
+ end
22
+
23
+ # Supports Enumerable, but not the sorting methods, as this class is a linked list structure.
24
+ # This is of limited usefulness, because you wouldn't use an iterator to invoke the procs
25
+ # in the chain, because each proc will invoke the next node arbitrarily or possibly not at all
26
+ # in the case of around advice!
27
+ class AdviceChainNode
28
+ include Enumerable
29
+ def initialize options = {}, &proc_block
30
+ @proc = Proc.new &proc_block
31
+ options[:next_node] ||= nil # assign so the attribute is always created
32
+ options.each do |key, value|
33
+ instance_variable_set "@#{key}".intern, value
34
+ (class << self; self; end).class_eval <<-EOF
35
+ attr_accessor(:#{key})
36
+ EOF
37
+ end
38
+ end
39
+
40
+ def call jp, *args
41
+ begin
42
+ @proc.call jp, *args
43
+ rescue => e
44
+ class_or_instance_method_separater = jp.is_instance_method? ? "#" : "."
45
+ context_message = "Exception raised while executing \"#{jp.context.advice_kind}\" advice for \"#{jp.type_or_object.inspect}#{class_or_instance_method_separater}#{jp.method_name}\": "
46
+ backtrace = e.backtrace
47
+ e2 = e.exception(context_message + e.message)
48
+ e2.set_backtrace backtrace
49
+ raise e2
50
+ end
51
+ end
52
+
53
+ # Supports Enumerable
54
+ def each
55
+ node = self
56
+ while node.nil? == false
57
+ yield node
58
+ node = node.next_node
59
+ end
60
+ end
61
+
62
+ def size
63
+ inject(0) {|memo, node| memo += 1}
64
+ end
65
+
66
+ def empty?
67
+ next_node.nil?
68
+ end
69
+
70
+ def inspect &block
71
+ block ? yield(self) : super
72
+ end
73
+
74
+ NIL_OBJECT = Aquarium::Utils::NilObject.new
75
+ end
76
+
77
+ class NoAdviceChainNode < AdviceChainNode
78
+ # Note that we extract the block passed to the original method call, if any,
79
+ # from the context and pass it to method invocation.
80
+ def initialize options = {}
81
+ super(options) { |jp, *args|
82
+ block_for_method = jp.context.block_for_method
83
+ invoking_object = jp.is_instance_method? ? jp.context.advised_object : jp.type
84
+ method = invoking_object.method(@alias_method_name)
85
+ block_for_method.nil? ?
86
+ method.call(*args) :
87
+ method.call(*args, &block_for_method)
88
+ }
89
+ end
90
+ end
91
+
92
+ class BeforeAdviceChainNode < AdviceChainNode
93
+ def initialize options = {}
94
+ super(options) { |jp, *args|
95
+ before_jp = jp.make_current_context_join_point :advice_kind => :before
96
+ advice.call(before_jp, *args)
97
+ next_node.call(jp, *args)
98
+ }
99
+ end
100
+ end
101
+
102
+ class AfterReturningAdviceChainNode < AdviceChainNode
103
+ def initialize options = {}
104
+ super(options) { |jp, *args|
105
+ returned_value = next_node.call(jp, *args)
106
+ next_jp = jp.make_current_context_join_point :advice_kind => :after_returning, :returned_value => returned_value
107
+ advice.call(next_jp, *args)
108
+ next_jp.context.returned_value # allow advice to modify the returned value
109
+ }
110
+ end
111
+ end
112
+
113
+ # Note that the advice is not invoked if the exception is not of a type specified when the advice was created.
114
+ # However, the default is to advise all thrown objects.
115
+ class AfterRaisingAdviceChainNode < AdviceChainNode
116
+ include Aquarium::Utils::ArrayUtils
117
+ def initialize options = {}
118
+ super(options) { |jp, *args|
119
+ begin
120
+ next_node.call(jp, *args)
121
+ rescue Object => raised_exception
122
+ if after_raising_exceptions_list_includes raised_exception
123
+ next_jp = jp.make_current_context_join_point :advice_kind => :after_raising, :raised_exception => raised_exception
124
+ advice.call(next_jp, *args)
125
+ raised_exception = next_jp.context.raised_exception # allow advice to modify raised exception
126
+ end
127
+ raise raised_exception
128
+ end
129
+ }
130
+ end
131
+
132
+ private
133
+ def after_raising_exceptions_list_includes raised_exception
134
+ after_raising_exceptions_list.find {|x| raised_exception.kind_of? x}
135
+ end
136
+
137
+ def after_raising_exceptions_list
138
+ list = make_array(@after_raising)
139
+ (list.nil? || list.empty? || (list.size == 1 && list[0] == "")) ? [Object] : list
140
+ end
141
+ end
142
+
143
+ class AfterAdviceChainNode < AdviceChainNode
144
+ def initialize options = {}
145
+ super(options) { |jp, *args|
146
+ # advice.call is invoked in each bloc, rather than once in an "ensure" clause, so the invocation in the rescue class
147
+ # can allow the advice to change the exception that will be raised.
148
+ begin
149
+ returned_value = next_node.call(jp, *args)
150
+ next_jp = jp.make_current_context_join_point :advice_kind => :after, :returned_value => returned_value
151
+ advice.call(next_jp, *args)
152
+ next_jp.context.returned_value # allow advice to modify the returned value
153
+ rescue Object => raised_exception
154
+ next_jp = jp.make_current_context_join_point :advice_kind => :after, :raised_exception => raised_exception
155
+ advice.call(next_jp, *args)
156
+ raise next_jp.context.raised_exception
157
+ end
158
+ }
159
+ end
160
+ end
161
+
162
+ class AroundAdviceChainNode < AdviceChainNode
163
+ def initialize options = {}
164
+ super(options) { |jp, *args|
165
+ around_jp = jp.make_current_context_join_point :advice_kind => :around, :proceed_proc => next_node
166
+ advice.call(around_jp, *args)
167
+ }
168
+ end
169
+ end
170
+
171
+ # The advice_kind argument must be one of the values returned by Advice.kinds or one of the special values
172
+ # ":no" or ":none", signfying a node for which there is no advice, where the actual method being advised is
173
+ # called directly instead. This kind of node is normally used as the terminal leaf in the chain.
174
+ module AdviceChainNodeFactory
175
+ def self.make_node options = {}
176
+ advice_kind = options[:advice_kind]
177
+ raise Aquarium::Utils::InvalidOptions.new("Unknown advice kind specified: #{advice_kind}") unless valid(advice_kind)
178
+ advice_kind = :no if advice_kind == :none
179
+ advice_chain_node_name = advice_kind.to_s.to_camel_case + "AdviceChainNode"
180
+ clazz = Aquarium::Aspects.const_get advice_chain_node_name
181
+ clazz.new options
182
+ end
183
+
184
+ def self.valid advice_kind
185
+ advice_kind == :no || advice_kind == :none || Advice.kinds.include?(advice_kind)
186
+ end
187
+ end
188
+ end
189
+ end