# Provides cheap advice mechanism for Ruby. class CheapAdvice attr_accessor :before, :after, :around, :advised @@advice_id = 0 NULL_PROC = lambda { | ar | } NULL_AROUND_PROC = lambda { | ar, result | result.call } EMPTY_HASH = { }.freeze # options: # :before # :after # :around def initialize opts, &blk @advised = [ ] opts_hash = EMPTY_HASH opts_key = nil case opts # advice :before => lambda ... when Hash opts_hash = opts # advice :method, :before do ... end when Symbol opts_key = opts end @before = (opts_key == :before ? blk : opts_hash[:before]) || NULL_PROC @after = (opts_key == :after ? blk : opts_hash[:after]) || NULL_PROC @around = (opts_key == :around ? blk : opts_hash[:around]) || NULL_AROUND_PROC @blk = blk end # Apply advice to class and method. def advise cls, method return cls.map { | x | advise x, method } if cls.kind_of?(Enumerable) return method.map { | x | advise cls, x } if method.kind_of?(Enumerable) old_method = "__advice_#{@@advice_id += 1}_#{method}" new_method = "__advice_#{@@advice_id += 1}_#{method}" before_method = "__advice_before_#{@@advice_id}_#{method}" after_method = "__advice_after_#{@@advice_id}_#{method}" around_method = "__advice_around_#{@@advice_id}_#{method}" advice = self advised = Advised.new( self, cls, method, old_method, new_method, before_method, after_method, around_method ) advised.apply_advice_methods cls.class_eval do define_method advised.new_method do | *args | ar = ActivationRecord.new(self, advised.method, args) do_result = Proc.new do self.send(advised.before_method, ar) begin ar.result = self.send(advised.old_method, *ar.args) ar.previous_called rescue Exception => err ar.error = err ensure self.send(advised.after_method, ar) end ar.result end self.send advised.around_method, ar, do_result raise ar.error if ar.error ar.result end end advised.advise @advised << advised advised end def unadvise @advised.each { | x | x.unadvise } end def readvise @advised.each { | x | x.advise } end # Represents the application of advice to a class and method. class Advised attr_reader :advice, :cls attr_reader :method, :old_method, :new_method attr_reader :before_method, :after_method, :around_method def initialize *args @advice, @cls, @method, @old_method, @new_method, @before_method, @after_method, @around_method = *args end def apply_advice_methods this = self @cls.instance_eval do define_method(this.before_method, &this.advice.before) define_method(this.after_method, &this.advice.after) define_method(this.around_method, &this.advice.around) end end def advise this = self @cls.instance_eval do alias_method this.old_method, this.method if method_defined? this.method and ! method_defined? this.old_method alias_method this.method, this.new_method end end def unadvise this = self @cls.instance_eval do alias_method this.method, this.old_method if method_defined? this.old_method end end end # Represents the activation record of a method invocation. class ActivationRecord attr_reader :rcvr, :method, :args attr_accessor :result, :error, :body attr_accessor :previous_called def initialize *args @rcvr, @method, @args = *args end end ###################################### # Testing # class Foo attr_accessor :foo def baz(arg) puts "in baz" 5 + arg end end class Bar attr_accessor :bar def baz(arg) puts "in baz" 7 + arg end end def self.test_me tracing = CheapAdvice.new(:around) do | ar, result | puts " TRACE: before #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")})" puts " foo = #{@foo.inspect}" puts " bar = #{@bar.inspect}" result = result.call puts " TRACE: after #{ar.rcvr.class}\##{ar.method}(#{ar.args.join(", ")}) => #{result.inspect}" ar.result = "yo!" puts " TRACE: return #{ar.result.inspect}" "boy!" end puts "\n With tracing advice:\n" tracing.advise( [Foo, Bar], [ :bar, :bar=, :baz ]) f = Foo.new b = Bar.new test_do_f(f, b) tracing.unadvise puts "\n Without tracing advice:\n" test_do_f(f, b) tracing end def self.test_do_f(f, b) puts "f.foo = 10 => #{(f.foo = 10).inspect}" puts "f.foo => #{(f.foo).inspect}" puts "f.baz(10) => #{(f.baz(10)).inspect}" puts "b.bar = 101 => #{(b.bar = 101).inspect}" puts "b.bar => #{(b.bar).inspect}" puts "b.baz(10) => #{(b.baz(10)).inspect}" end end