#
# Project:: Ruby-Snippets
#
# Author:: Christoph Heindl (mailto:christoph.heindl@gmail.com)
# Homepage:: http://cheind.blogspot.com
#
# == Overview
#
# Implements <tt>FollowingHook#following</tt>. A
# utility to hook into instance methods and execute
# custom code after hooked methods are called.
#
# See discussion at
# http://cheind.blogspot.com/2008/12/method-hooks-in-ruby.html
#
# Contains methods to hook method calls
module FollowingHook
module ClassMethods
private
# Hook the provided instance methods so that the block
# is executed directly after the specified methods have been invoked.
#
# There can only be one hook for a single instance method. Further attempts
# to hook already hooked methods will be silently ignored.
#
# +syms+ is list of method symbols or stringified names to hook on
# +block+ is the block to execute after hooked method has been invoked.
# Block will receive two arguments: the receiver of the method and an argument hash
# which contains the invoked method name <tt>:method</tt>, the arguments passed to
# the method <tt>:args</tt> and the return value of the method <tt>:return</tt>.
#
# class Object
# include FollowingHook
# following :system do |receiver, args|
# p "#{args[:method]} called with arguments #{args[:args].join(",")}"
# p "return value was #{args[:return]}"
# end
# end
#
# system('ruby --version')
# # => ruby 1.8.6 (2008-08-11 patchlevel 287) [i386-mswin32]
# # => "system called with arguments ruby --version"
# # => "return value was true"
# # => true
#
def following(*syms, &block)
syms.each do |sym| # For each symbol
str_id = "__#{sym}__hooked__"
unless private_instance_methods.include?(str_id)
alias_method str_id, sym # Backup original method
private str_id # Make backup private
define_method sym do |*args| # Replace method
ret = __send__ str_id, *args # Invoke backup
block.call(self, # Invoke hook
:method => sym,
:args => args,
:return => ret
)
ret # Forward return value of method
end
end
end
end
end
# On inclusion, we extend the receiver by the defined class-methods
# This is an ruby idiom for defining class methods within a module.
def FollowingHook.included(base)
base.extend(ClassMethods)
end
end
Refactorings
No refactoring yet !
raggi
December 8, 2008, December 08, 2008 11:24, permalink
I wouldn't use that, instead if at all, I'd probably use "after" as defined below.
Also, you might want to look into the way task execution occurs in Rake.
I find redefining methods like this tends to become unmanageable when there are other ways to invoke code that don't require permanently damaging the object protocol that you're trying to hook.
Anyway, 1.9 support too.
module FollowingHook
module ClassMethods
private
def following(*syms, &block)
syms.each do |sym| # For each symbol
hook_method = RUBY_VERSION >= '1.9.0' ? :"__#{sym}__hooked__" : "__#{sym}__hooked__"
raise ArgumentError, 'hook already exists' if private_instance_methods.include?(hook_method)
alias_method hook_method, sym
private hook_method
define_method sym do |*args|
after sym, *args, &block
end
end
end
end
def after(method, *args)
yield :method => method, :args => args, :return => send(:"__#{method}__hooked__", *args)
end
def self.included(base)
base.extend(ClassMethods)
end
end
Christoph Heindl
December 9, 2008, December 09, 2008 07:44, permalink
raggi,
thanks for your input!
I browsed the changes for #alias_method in ruby 1.9 (http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9) but could not find any hints on #alias_method requiring symbols. http://www.ruby-doc.org/core-1.9/index.html misses that part too.
Guess I have to install it :)
I agree that hooking method may produce an environment that is hard to maintain, but sometimes (maybe rarely) hooking is just what gets you the job done quickly and does not cluster your code see contrived example below, don't you think?
Best regards,
christoph
require 'logger'
require 'util/following'
$Log = Logger.new(STDOUT)
$Log.level = Logger::DEBUG
class Window
attr_accessor :text
attr_reader :renderer
if $Log.level <= Logger::DEBUG
include FollowingHook
following :text=, :text, :renderer do |sender, args|
$Log.debug("#{sender} called with arguments #{args[:args].join(',')}")
end
end
end
Tj Holowaychuk
December 10, 2008, December 10, 2008 17:34, permalink
Not sure if this really is what your looking for but, In similar situations I tend to create a module, and then use the ancestor chain to super back to the original functionality like below. Probably not the best solution either but it worked well for me.
require 'fileutils'
module VerboseFileUtils
include FileUtils
##
# Wrap _methods_ with _action_ log message.
def self.log action, *methods
methods.each do |meth|
define_method meth do |*args|
Commander::UI.log "#{action}", *args
super
end
end
end
log "remove", :rm, :rm_r, :rm_rf, :rmdir
log "create", :touch, :mkdir, :mkdir_p
log "copy", :cp, :cp_r
log "move", :mv
log "change", :cd
log "link", :ln, :ln_s
log "install", :install
end
include VerboseFileUtils
Outputs:
create this
remove that
move foo
link bar
Tj Holowaychuk
December 10, 2008, December 10, 2008 17:34, permalink
GAH! the output portion did not work at all.. haha you get the point
Christoph Heindl
December 10, 2008, December 10, 2008 20:23, permalink
Using the ancestor chain + overriding existing to call ancestor is a nice idea. Thanks for sharing your thoughts.
Mario Freitas
October 15, 2010, October 15, 2010 04:15, permalink
I basically used raggi's code for dealing with hooks and modified it to use the concept of "filters".
#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
module ClassMethods
def remove_filter (type, *syms)
@@__filters__ ||= { :before => {}, :after => {} }
syms.each { |sym| @@__filters__[type][sym] = [] }
end
private
def filter (type, *syms, &block)
syms.each do |sym|
@@__filters__ ||= { :before => {}, :after => {} }
@@__filters__.each_key { |k| @@__filters__[k][sym] ||= [] }
@@__filters__[type][sym] << block
filter = "__#{sym}__filter__".to_sym
next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
alias_method filter, sym
private filter
define_method sym do |*args|
call = { :method => sym, :args => args }
@@__filters__[:before][sym].each do |b|
send :execute_filter, :before, call, &b
return call[:return] if call.has_key?(:return)
end
call[:return] = send filter, *call[:args]
@@__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
return call[:return]
end
end
end
def before_filter (*syms, &block)
filter(:before, *syms, &block)
end
def after_filter (*syms, &block)
filter(:after, *syms, &block)
end
end
def execute_filter (type, call)
call[:type] = type
yield call, *call[:args]
end
def self.included (base)
base.extend(ClassMethods)
end
end
#
# Place in same folder as method_filter.rb
#
require File.join(File.dirname(File.expand_path(__FILE__)),"method_filter")
class Foo
include MethodFilter
def func (x, y, z)
puts "func(#{x}, #{y}, #{z})"
r = x * y + z
puts "func = #{r}"
r
end
after_filter(:func) do |call, x, y, z|
puts "Multiplying return value of func by 2"
call[:return] *= 2
end
after_filter(:func) do |call, *args|
puts "#{call[:method]}'s #{call[:type]} filter called with #{args.inspect}"
end
before_filter(:func) do |call, *args|
puts "#{call[:method]}'s #{call[:type]} filter called with #{args.inspect}"
# if you want to exit the filters chain and return a value immediately,
# you can define call[:return] to something (including nil) and that will
# be returned to the caller.
end
before_filter(:func) do |call, x, y, z|
puts "Reversing order of arguments passed to func"
call[:args] = [ z, y, x ]
end
end
f = Foo.new
puts "ALL filters enabled"
puts f.func(1,2,3)
puts "AFTER filters disabled"
Foo.remove_filter(:after, :func)
puts f.func(1,2,3)
puts "ALL filters disabled"
Foo.remove_filter(:before, :func)
puts f.func(1,2,3)
ALL filters enabled func's before filter called with [1, 2, 3] Reversing order of arguments passed to func func(3, 2, 1) func = 7 Multiplying return value of func by 2 func's after filter called with [3, 2, 1] 14 AFTER filters disabled func's before filter called with [1, 2, 3] Reversing order of arguments passed to func func(3, 2, 1) func = 7 7 ALL filters disabled func(1, 2, 3) func = 5 5
Mario Freitas
October 15, 2010, October 15, 2010 07:18, permalink
I forgot to include an :instance key in the "call" hash so that you can access the instance's methods.
In method_filter.rb, just change:
call = { :method => sym, :args => args }
to:
call = { :method => sym, :args => args, :instance => self }
and then you can use call[:instance].some_method as if it was "self.some_method" (i.e., execute "some_method" in the context of the instance whose filtered method is being executed).
The fixed code follows below anyway.
#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
module ClassMethods
def remove_filter (type, *syms)
@@__filters__ ||= { :before => {}, :after => {} }
syms.each { |sym| @@__filters__[type][sym] = [] }
end
private
def filter (type, *syms, &block)
syms.each do |sym|
@@__filters__ ||= { :before => {}, :after => {} }
@@__filters__.each_key { |k| @@__filters__[k][sym] ||= [] }
@@__filters__[type][sym] << block
filter = "__#{sym}__filter__".to_sym
next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
alias_method filter, sym
private filter
define_method sym do |*args|
call = { :method => sym, :args => args, :instance => self }
@@__filters__[:before][sym].each do |b|
send :execute_filter, :before, call, &b
return call[:return] if call.has_key?(:return)
end
call[:return] = send filter, *call[:args]
@@__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
return call[:return]
end
end
end
def before_filter (*syms, &block)
filter(:before, *syms, &block)
end
def after_filter (*syms, &block)
filter(:after, *syms, &block)
end
end
def execute_filter (type, call)
call[:type] = type
yield call, *call[:args]
end
def self.included (base)
base.extend(ClassMethods)
end
end
Mario Freitas
October 18, 2010, October 18, 2010 02:19, permalink
I am sorry for the previous posts, as I was kind in a hurry and had not tested the code carefully.
Fixed several bugs. Should be better now.
#
# Based on raggi's refactored version of the FollowingHook module's code.
# http://refactormycode.com/codes/656-method-hooks-in-ruby-any-cleaner
#
# Main changes:
# - Removed Ruby version test (Ruby 1.8 and 1.9 both work).
# - Support not only "after" but also "before" hooks.
# - Transformed "hooks" into "filters" (allow changing of args, and return values).
# - Allow multiple filters to be installed and uninstalled.
#
module MethodFilter
module ClassMethods
def remove_filter (type, *syms)
syms.each { |sym| __filters__[type][sym] = [] }
end
def __filters__
return class_variable_get(:@@__filters__) if class_variable_defined?(:@@__filters__)
class_variable_set(:@@__filters__, {:before => {}, :after => {}})
end
private
def filter (type, *syms, &block)
syms.each do |sym|
__filters__.each_key { |k| __filters__[k][sym] ||= [] }
__filters__[type][sym] << block
filter = "__#{sym}__filter__".to_sym
next if private_instance_methods.map { |m| m.to_sym }.include?(filter)
alias_method filter, sym
private filter
define_method sym do |*args|
call = { :method => sym, :args => args, :instance => self }
self.class.__filters__[:before][sym].each do |b|
send :execute_filter, :before, call, &b
break if call.has_key?(:return)
end
unless call.has_key?(:return)
call[:return] = send filter, *call[:args]
self.class.__filters__[:after][sym].each { |b| send :execute_filter, :after, call, &b }
end
call[:return]
end
end
end
def before_filter (*syms, &block)
filter(:before, *syms, &block)
end
def after_filter (*syms, &block)
filter(:after, *syms, &block)
end
end
def execute_filter (type, call)
call[:type] = type
yield call, *call[:args]
end
def self.included (base)
base.extend(ClassMethods)
end
end
The following module provides method hooks (code that is called after a method has been invoked) for specified methods. Is that readable of general use for you? For more examples and unit tests please follow the discussion at http://cheind.blogspot.com/2008/12/method-hooks-in-ruby.html . Criticism welcome! Thanks!