cheap_advice 1.0.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,51 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'cheap_advice'
4
+ require 'pp'
5
+ require 'time'
6
+
7
+ class MyClass
8
+ def foo
9
+ 42
10
+ end
11
+ end
12
+
13
+ class MyOtherClass
14
+ def bar
15
+ 43
16
+ end
17
+ end
18
+
19
+ a = MyClass.new
20
+ b = MyOtherClass.new
21
+
22
+ trace_advice = CheapAdvice.new(:around) do | ar, body |
23
+ ar.advice[:log] << "#{Time.now.iso8601(6)} " <<
24
+ "#{ar.rcvr.class} #{ar.meth} #{ar.rcvr.object_id}\n"
25
+ body.call
26
+ ar.advice[:log] << "#{Time.now.iso8601(6)} " <<
27
+ "#{ar.rcvr.class} #{ar.meth} #{ar.rcvr.object_id} " <<
28
+ "=> #{ar.result.inspect}\n"
29
+ end
30
+ trace_advice[:log] = $stderr # File.open("trace.log", "a+")
31
+
32
+ puts "\nWithout advice:"
33
+ pp a.foo
34
+ pp b.bar
35
+
36
+ puts "\nWith advice enabled:"
37
+ trace_advice.advise!(MyClass, :foo)
38
+ trace_advice.advise!(MyOtherClass, :bar)
39
+ pp a.foo
40
+ pp b.bar
41
+
42
+ puts "\nWith advice disabled:"
43
+ trace_advice.disable!
44
+ pp a.foo
45
+ pp b.bar
46
+
47
+ puts "\nWith advice re-enabled:"
48
+ trace_advice.enable!
49
+ pp a.foo
50
+ pp b.bar
51
+
@@ -0,0 +1,17 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ 'MyClass':
11
+ :advice: trace
12
+
13
+ 'MyClass#foo':
14
+ :enabled: false
15
+
16
+ 'MyClass#bar':
17
+ :enabled: false
@@ -0,0 +1,17 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+
16
+ MyClass#bar:
17
+ :enabled: false
@@ -0,0 +1,23 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: true
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+ :default:
10
+ :log_prefix: "YO "
11
+
12
+ MyClass:
13
+ :advice: trace
14
+
15
+ MyClass#foo: ~
16
+ MyClass#bar:
17
+ :options:
18
+ :trace:
19
+ :logger:
20
+ :name: :alternate
21
+ :log_after: false
22
+ :log_result: false
23
+
@@ -0,0 +1,20 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: false
15
+
16
+ MyClass#bar:
17
+ :enabled: false
18
+
19
+ NoClass#no_method:
20
+ :enabled: true
@@ -0,0 +1,22 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+ :options:
16
+ :trace:
17
+ :log_before: false
18
+ :log_after: true
19
+ :log_result: true
20
+
21
+ MyClass#bar: false
22
+
@@ -0,0 +1,25 @@
1
+
2
+ :advice:
3
+ ~:
4
+ :enabled: false
5
+ :options:
6
+ :trace:
7
+ :logger:
8
+ :name: :default
9
+
10
+ MyClass:
11
+ :advice: trace
12
+
13
+ MyClass#foo:
14
+ :enabled: true
15
+ :options:
16
+ :trace:
17
+ :log_before: true
18
+ :log_args: true
19
+ :log_after: true
20
+ :log_result: false
21
+
22
+ MyClass#bar: false
23
+
24
+ MyClass#raise_error: true
25
+
@@ -0,0 +1,99 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'cheap_advice'
4
+ require 'cheap_advice/configuration'
5
+ require 'cheap_advice/trace'
6
+ require 'yaml'
7
+ require 'benchmark'
8
+
9
+ require 'rubygems'
10
+ =begin
11
+ gem 'ruby-debug'
12
+ require 'ruby-debug'
13
+ =end
14
+
15
+ ##################################################
16
+ # Target
17
+
18
+ class MyClass
19
+ def foo(arg)
20
+ 42 + arg
21
+ end
22
+ def bar(arg)
23
+ 24 + arg
24
+ end
25
+ def raise_error(arg)
26
+ raise ArgumentError, "#{arg.inspect}"
27
+ end
28
+ end
29
+
30
+ class MySubclass < MyClass
31
+ end
32
+
33
+ trace_advice = nil
34
+ trace_config = nil
35
+
36
+ Benchmark.bm(40) do | bm |
37
+
38
+ ##################################################
39
+ # Advice
40
+
41
+ bm.report("trace_advice setup") do
42
+ trace_advice = CheapAdvice::Trace.new
43
+
44
+ # Configure Trace loggers by name.
45
+ trace_advice.logger[:default] = {
46
+ :target => File.open(File.expand_path("../ex02-default.log", __FILE__), "w"),
47
+ }
48
+
49
+ trace_advice.logger[:alternate] = {
50
+ :target => File.open(File.expand_path("../ex02-alternate.log", __FILE__), "w"),
51
+ :formatter => CheapAdvice::Trace::YamlFormatter
52
+ }
53
+ end
54
+
55
+ ##################################################
56
+ # Advice Configuration
57
+
58
+
59
+ bm.report("trace_config setup") do
60
+ # Register Trace advice with configuration.
61
+ trace_config = CheapAdvice::Configuration.new
62
+ trace_config.advice[:trace] = trace_advice
63
+ end
64
+
65
+ config_yml = File.expand_path("../ex02-trace-*.yml", __FILE__)
66
+
67
+ Dir[config_yml].sort.each do | config_yml |
68
+ # Configure.
69
+ config_hash = YAML.load_file(config_yml)
70
+ trace_config.config = config_hash[:advice]
71
+
72
+ bm.report("configure! using #{File.basename(config_yml)}") do
73
+ trace_config.configure!
74
+ end
75
+
76
+ msg = "Using #{config_yml}"
77
+ # puts msg
78
+ trace_advice.log_all(msg)
79
+
80
+ ##################################################
81
+ # Target activity
82
+
83
+ bm.report("Target activity") do
84
+ a = MyClass.new
85
+ a.foo(123)
86
+ a.bar(456)
87
+ b = MySubclass.new
88
+ b.foo(789)
89
+ b.bar(012)
90
+ begin
91
+ a.raise_error(:aSymbol)
92
+ rescue Exception
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+
@@ -0,0 +1,511 @@
1
+ require 'thread'
2
+
3
+ # Provides cheap advice mechanism for Ruby.
4
+ # See: http://github.com/kstephens/cheap_advice
5
+ #
6
+ class CheapAdvice
7
+ EMPTY_Hash = { }.freeze
8
+ EMPTY_Array = [ ].freeze
9
+ EMPTY_String = ''.freeze
10
+
11
+ class Error < ::Exception; end
12
+
13
+ module Options
14
+ # Hash accesible by #[] and #[]=.
15
+ attr_accessor :options
16
+ def initialize
17
+ @mutex = Mutex.new
18
+ @options = { }
19
+ end
20
+
21
+ def [](k)
22
+ # @mutex.synchronize do
23
+ @options[k]
24
+ # end
25
+ end
26
+
27
+ def []=(k, v)
28
+ # @mutex.synchronize do
29
+ @options[k] = v
30
+ # end
31
+ end
32
+ end
33
+ include Options
34
+
35
+ # Procs called before, after, and around the original method.
36
+ attr_accessor :before, :after, :around
37
+
38
+ # Collection of Advised method bindings.
39
+ attr_accessor :advised
40
+
41
+ # Module or Array of Modules to extend new Advised objects with.
42
+ attr_accessor :advised_extend
43
+
44
+ NULL_PROC = lambda { | ar | }
45
+ NULL_AROUND_PROC = lambda { | ar, body | body.call }
46
+
47
+ # options:
48
+ # :before
49
+ # :after
50
+ # :around
51
+ def initialize *opts, &blk
52
+ super()
53
+
54
+ @advised = [ ]
55
+ @advised_for = { }
56
+
57
+ opts_hash = Hash === opts[-1] ? opts.pop : { }
58
+ opts_key = opts.shift
59
+ @options = opts_hash
60
+
61
+ @before = (opts_key == :before ? blk : opts_hash[:before]) ||
62
+ NULL_PROC
63
+ @after = (opts_key == :after ? blk : opts_hash[:after]) ||
64
+ NULL_PROC
65
+ @around = (opts_key == :around ? blk : opts_hash[:around]) ||
66
+ NULL_AROUND_PROC
67
+
68
+ @blk = blk
69
+ end
70
+
71
+
72
+ # Apply advice a method on a Module (or Class).
73
+ #
74
+ # The advised method is enabled immediately (this may change in a future release).
75
+ #
76
+ # Returns an Advised object that describes what method was advised.
77
+ #
78
+ # mod can be a String, a Module or an Array of either.
79
+ # method can be a String, a Symbol or an Array of either.
80
+ # if either are Arrays the result will be an Array of Advised objects.
81
+ #
82
+ # The type of method scope can be specified by:
83
+ # * :instance (default)
84
+ # * :class
85
+ # * :module
86
+ #
87
+ # Any additional Hash options are propagated to the Advised binding object, which
88
+ # can be accessed from the ActivationRecord passed to the Advice block(s).
89
+ #
90
+ # Examples:
91
+ # advice = CheapAdvice(:around) { | ar, body | ...; body.call; ... }
92
+ # advice.advise! MyClass, :instance_method, options_hash
93
+ # advice.advise! MyClass, :class_method, :class
94
+ #
95
+ # Each Advised object is extended with #advised_extend.
96
+ # The #advised Array lists all Advised object.
97
+ #
98
+ def advise! mod, method, *opts
99
+ return mod.map { | x | advise! x, method, *opts } if
100
+ Array === mod
101
+ return method.map { | x | advise! mod, x, *opts } if
102
+ Array === method
103
+
104
+ opts_hash = Hash === opts[-1] ? opts.pop : { }
105
+ kind = opts.shift
106
+ kind ||= :instance
107
+
108
+ method = method.to_sym
109
+
110
+ @mutex.synchronize do
111
+ unless @enabled_once
112
+ self.enabled!
113
+ @enabled_once = true
114
+ end
115
+
116
+ advised = advised_for mod, method, kind, opts_hash
117
+
118
+ advised.enable! # Should this really be automatically enabled??
119
+
120
+ advised
121
+ end
122
+ end
123
+
124
+ # Called once the first time this advice is enabled.
125
+ # Instances can override this method.
126
+ def enabled!
127
+ self
128
+ end
129
+
130
+ def advised_select mod, meth, kind
131
+ @advised.select do | ad |
132
+ (mod ? mod == ad.mod : true) &&
133
+ (meth ? meth == ad.meth : true) &&
134
+ (kind ? kind == ad.kind : true)
135
+ end
136
+ end
137
+
138
+ # Returns the existing Advised binding or creates a new one.
139
+ def advised_for mod, meth, kind, opts
140
+ (@advised_for[[ mod, meth, kind ]] ||=
141
+ construct_advised_for(mod, meth, kind)).set_options!(opts)
142
+ end
143
+
144
+ # Constructs an Advised binding from this Advice.
145
+ def construct_advised_for mod, meth, kind
146
+ advice = self
147
+
148
+ advised = Advised.new(advice, mod, meth, kind)
149
+
150
+ case @advised_extend
151
+ when nil
152
+ when Module
153
+ advised.extend(@advised_extend)
154
+ when Array
155
+ @advised_extend.each { | m | advised.extend(m) }
156
+ else
157
+ raise TypeError, "advised_extend: expected nil, Module or Array of Modules, given #{@advised_extend.class}"
158
+ end
159
+
160
+ advised.register_advice_methods!
161
+
162
+ advised.define_new_method!
163
+
164
+ @advised << advised
165
+
166
+ advised
167
+ end
168
+
169
+
170
+ # Disables all currently Advised methods.
171
+ def disable!
172
+ @mutex.synchronize do
173
+ @advised.each { | x | x.disable! }
174
+ end
175
+ self
176
+ end
177
+ alias :unadvise! :disable!
178
+
179
+
180
+ # Enables all currently Advised methods.
181
+ def enable!
182
+ @mutex.synchronize do
183
+ @advised.each { | x | x.enable! }
184
+ end
185
+ self
186
+ end
187
+ alias :readvise! :enable!
188
+
189
+
190
+ # Represents the application/binding of advice to a class and method.
191
+ class Advised
192
+ include Options
193
+
194
+ @@mutex = Mutex.new
195
+ @@advised_id ||= 0
196
+
197
+ # The Advice being applied to the Module and method.
198
+ attr_reader :advice
199
+ # The Module, method and kind (instance, class or module method)
200
+ attr_reader :mod, :meth, :kind
201
+ alias :cls :mod # Deprecated.
202
+
203
+ # The unique Advised id used to generate unique method names.
204
+ attr_reader :advised_id
205
+
206
+ # The name of the old and new method being patched in.
207
+ attr_reader :old_meth, :new_meth
208
+
209
+ # The name of the before, after and around methods.
210
+ attr_reader :before_meth, :after_meth, :around_meth
211
+
212
+ # True if the Advised methods are currently installed.
213
+ attr_reader :enabled
214
+
215
+ def initialize *args
216
+ @mutex = Mutex.new
217
+ @advice, @mod, @meth, @kind, @options = *args
218
+
219
+ case @kind
220
+ when :instance, :class, :module
221
+ else
222
+ raise ArgumentError, "invalid kind #{kind.inspect}"
223
+ end
224
+
225
+ @options ||= { }
226
+
227
+ @@mutex.synchronize do
228
+ @advised_id = @@advised_id += 1
229
+ end
230
+
231
+ @old_meth = :"__advice_old_#{@@advised_id}_#{@meth}"
232
+ @new_meth = :"__advice_new_#{@@advised_id}_#{@meth}"
233
+
234
+ @before_meth = :"__advice_before_#{@@advised_id}_#{@meth}"
235
+ @after_meth = :"__advice_after_#{@@advised_id}_#{@meth}"
236
+ @around_meth = :"__advice_around_#{@@advised_id}_#{@meth}"
237
+
238
+ @enabled =
239
+ @advice_methods_applied = false
240
+ end
241
+
242
+ def set_options! options
243
+ @options = options || { }
244
+ self
245
+ end
246
+
247
+ INSTANCE_SEP = '#'.freeze
248
+ MODULE_SEP = '.'.freeze
249
+
250
+ # The string name for the method.
251
+ # Returns "Foo#bar" for an instance method named :bar on class Foo.
252
+ # Returns "Foo.bar" for a class or module method named .bar on class Foo.
253
+ def meth_to_s
254
+ @meth_to_s ||=
255
+ "#{@mod}#{@kind == :instance ? INSTANCE_SEP : MODULE_SEP}#{@meth}".freeze
256
+ end
257
+
258
+ # True if the advice, mod, method and kind are equal.
259
+ def == x
260
+ return false unless self.class === x
261
+ @advice == x.advice && @mod == x.mod && @meth == x.meth && @kind == x.kind
262
+ end
263
+
264
+ # Support for Hash.
265
+ def hash
266
+ @advice.hash ^ @mod.hash ^ @meth.hash ^ @kind.hash
267
+ end
268
+
269
+ # Returns the target Module for the kind of method.
270
+ def mod_target
271
+ case @kind
272
+ when :instance
273
+ mod_resolve
274
+ when :class, :module
275
+ (class << mod_resolve; self; end)
276
+ else
277
+ raise ArgumentError, "mod_target: invalid kind #{kind.inspect}"
278
+ end
279
+ end
280
+
281
+ # Resolves mod Strings to the actual target Module.
282
+ def mod_resolve
283
+ case @mod
284
+ when Module
285
+ @mod
286
+ when String, Symbol
287
+ @mod.to_s.split('::').
288
+ reject { | name | name.empty?}.
289
+ inject(Object) { | namespace, name | namespace.const_get(name) }
290
+ else
291
+ raise TypeError, "mod_resolve: expected Module, String, Symbol, given #{@mod.class}"
292
+ end
293
+ end
294
+
295
+ # Registers the before, after and around advice methods.
296
+ def register_advice_methods!
297
+ scope # force calculation of scope before aliasing methods.
298
+
299
+ @mutex.synchronize do
300
+ return self if @advice_methods_registered
301
+
302
+ this = self
303
+ mod_target.instance_eval do
304
+ define_method(this.before_meth, &this.advice.before)
305
+ define_method(this.after_meth, &this.advice.after)
306
+ define_method(this.around_meth, &this.advice.around)
307
+ end
308
+
309
+ @advice_methods_registered = true
310
+ end
311
+ self
312
+ end
313
+
314
+ # Defines the new advised method in the target Module.
315
+ def define_new_method!
316
+ advised = self
317
+
318
+ advised.mod_target.instance_eval do
319
+ define_method advised.new_meth do | *args, &block |
320
+ ar = ActivationRecord.new(advised, self, args, block)
321
+
322
+ # Proc to invoke the old method with :before and :after advise hooks.
323
+ body = Proc.new do
324
+ self.__send__(advised.before_meth, ar)
325
+ begin
326
+ ar.result = self.__send__(advised.old_meth, *ar.args, &ar.block)
327
+ rescue ::Object => err
328
+ ar.error = err
329
+ ensure
330
+ self.__send__(advised.after_meth, ar)
331
+ end
332
+ ar.result
333
+ end
334
+
335
+ # Invoke the :around advice with the body Proc.
336
+ self.__send__(advised.around_meth, ar, body)
337
+
338
+ # Reraise Exception, if occured.
339
+ raise ar.error if ar.error
340
+
341
+ # Return the message result to caller.
342
+ ar.result
343
+ end # define_method
344
+ end # instance_eval
345
+
346
+ self
347
+ end
348
+
349
+ if RUBY_VERSION =~ /^1\.8/
350
+ def scope
351
+ @scope ||= @mutex.synchronize do
352
+ this = self
353
+ mod_target.instance_eval do
354
+ case
355
+ when private_instance_methods(false).include?(this.meth.to_s)
356
+ :private
357
+ when protected_instance_methods(false).include?(this.meth.to_s)
358
+ :protected
359
+ else
360
+ :public
361
+ end
362
+ end
363
+ end
364
+ end
365
+ else
366
+ def scope
367
+ @scope ||= @mutex.synchronize do
368
+ this = self
369
+ mod_target.instance_eval do
370
+ case
371
+ when private_instance_methods(false).include?(this.meth)
372
+ :private
373
+ when protected_instance_methods(false).include?(this.meth)
374
+ :protected
375
+ else
376
+ :public
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
382
+
383
+ # Enables the advice on this method.
384
+ def enable!
385
+ @mutex.synchronize do
386
+ return self if @enabled
387
+
388
+ this = self
389
+ mod_target.instance_eval do
390
+ case this.scope
391
+ when :public
392
+ else
393
+ public this.meth
394
+ end
395
+
396
+ alias_method this.old_meth, this.meth if
397
+ ! method_defined? this.old_meth
398
+
399
+ alias_method this.meth, this.new_meth
400
+
401
+ case this.scope
402
+ when :private
403
+ private this.meth
404
+ when :protected
405
+ protected this.meth
406
+ end
407
+ end
408
+
409
+ enabled!
410
+ @enabled = true
411
+ end
412
+ self
413
+ end
414
+ alias :advise! :enable!
415
+
416
+ # Disables the advice on this method.
417
+ def disable!
418
+ @mutex.synchronize do
419
+ return self if ! @enabled
420
+
421
+ this = self
422
+ mod_target.instance_eval do
423
+ if method_defined? this.old_meth
424
+ alias_method this.meth, this.old_meth
425
+
426
+ case this.scope
427
+ when :private
428
+ private this.meth
429
+ when :protected
430
+ protected this.meth
431
+ end
432
+ end
433
+ end
434
+
435
+ disabled!
436
+ @enabled = false
437
+ end
438
+
439
+ self
440
+ end
441
+ alias :unadvise! :disable!
442
+
443
+ # Called when Advised is enabled.
444
+ # Instances can override this method.
445
+ def enabled!
446
+ self
447
+ end
448
+
449
+ # Called when Advised is enabled.
450
+ # Instances can override this method.
451
+ def disabled!
452
+ self
453
+ end
454
+
455
+ end
456
+
457
+
458
+ # Represents the activation record of a method invocation.
459
+ class ActivationRecord
460
+ # The Advised method binding object.
461
+ attr_reader :advised
462
+
463
+ # The original message receiver, arguments and block (if given).
464
+ # Can be modified by the advice blocks.
465
+ attr_accessor :rcvr, :args, :block
466
+
467
+ # The original message return result available in the :around or :after advice blocks.
468
+ # Value can be changed in the advice blocks to alter the return result.
469
+ attr_accessor :result
470
+
471
+ # Any Exception rescued from the original method.
472
+ # Usually nil if no exception was raised;
473
+ # if not nil, Exception is reraised after the :around advice block.
474
+ # Can be modified by the :after advice block.
475
+ attr_accessor :error
476
+ alias :exception :error
477
+ alias :exception= :error=
478
+
479
+ # Arbitrary data accessed by #[], #[]=.
480
+ # Advice blocks can use this to store arbitrary data.
481
+ # May be a frozen, empty Hash.
482
+ attr_accessor :data
483
+
484
+ def initialize *args
485
+ @advised, @rcvr, @args, @block = *args
486
+ end
487
+
488
+ def data
489
+ @data || EMPTY_Hash
490
+ end
491
+
492
+ def [] key
493
+ (@data || EMPTY_Hash)[key]
494
+ end
495
+
496
+ def []= key, value
497
+ (@data ||= { })[key] = value
498
+ end
499
+
500
+ # This methods are delegated to #advised.
501
+ DELEGATE_TO_ADVISED = [ :advice, :mod, :meth, :kind, :meth_to_s ].freeze
502
+ eval(DELEGATE_TO_ADVISED.map{|m| "def #{m}; @advised.#{m}; end; "} * "\n")
503
+
504
+ # The call stack with CheapAdvice methods filtered out.
505
+ def caller(offset = 0)
506
+ ::Kernel.caller(offset + 2)
507
+ end
508
+ end
509
+
510
+ end
511
+