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