Skip to content

Instantly share code, notes, and snippets.

@JoshCheek
Created August 24, 2012 12:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save JoshCheek/3450271 to your computer and use it in GitHub Desktop.
Save JoshCheek/3450271 to your computer and use it in GitHub Desktop.
Creates before/after hooks for methods
# a response to www.ruby-forum.com/topic/4405076
module Hook
def before(meth_name, &callback)
add_hook :before, meth_name, &callback
end
def after(meth_name, &callback)
add_hook :after, meth_name, &callback
end
def hooks
@hooks ||= Hash.new do |hash, method_name|
hash[method_name] = { before: [], after: [], hijacked: false }
end
end
def add_hook(where, meth_name, &callback)
hooks[meth_name][where] << callback
ensure_hijacked meth_name
end
def method_added(meth_name)
ensure_hijacked meth_name if hooks.has_key? meth_name
end
def ensure_hijacked(meth_name)
return if hooks[meth_name][:hijacked] || !instance_methods.include?(meth_name)
meth = instance_method meth_name
_hooks = hooks
_hooks[meth_name][:hijacked] = true
define_method meth_name do |*args, &block|
_hooks[meth_name][:before].each &:call
return_value = meth.bind(self).call *args, &block
_hooks[meth_name][:after].each &:call
return_value
end
end
end
describe Hook do
let :klass do
Class.new do
extend Hook
singleton_class.extend Hook
def self.meth(seen)
seen << "meth"
end
def meth(seen)
seen << "meth"
end
end
end
let(:instance) { klass.new }
let(:array) { [] }
it 'can be applied before and after instance methods' do
klass.before(:meth) { array << "before" }
klass.after(:meth) { array << "after" }
instance.meth array
array.should == %w[before meth after]
end
it 'can be applied before and after class methods' do
klass.singleton_class.before(:meth) { array << 'before' }
klass.singleton_class.after(:meth) { array << 'after'}
klass.meth array
array.should == %w[before meth after]
end
specify 'the method gets the params correctly and returns the return value correctly' do
klass.class_eval { def meth(*args, &block) [args, block] end }
klass.before(:meth) { }
klass.after(:meth) { }
return_val = instance.meth(1, 2, 3) { 10 }
return_val.first.should == [1, 2, 3]
return_val.last.call.should == 10
end
specify 'multiple before/after blocks can be declared and are executed in the order of declaration' do
klass.before(:meth) { array << "before1" }
klass.before(:meth) { array << "before2" }
klass.after(:meth) { array << "after1" }
klass.after(:meth) { array << "after2" }
instance.meth array
array.should == %w[before1 before2 meth after1 after2]
end
specify "instance and class methods don't conflict" do
klass.before(:meth) { array << "before instance" }
klass.singleton_class.extend Hook
klass.singleton_class.before(:meth) { array << 'before class' }
klass.meth array
instance.meth array
array.should == ['before class', 'meth', 'before instance', 'meth']
end
specify 'hooks can be before or after method definitions' do
array = []
klass.class_eval do
before(:meth2) { array << 'before1' }
after(:meth2) { array << 'after1' }
define_method(:meth2) { array << 'meth' }
before(:meth2) { array << 'before2' }
after(:meth2) { array << 'after2' }
end
klass.new.meth2
array.should == %w[before1 before2 meth after1 after2]
end
end
@JoshCheek
Copy link
Author

Just realized this probably doesn't work with subclasses / method redefinitions, but don't have time right now to look into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment