Creates before/after hooks for methods
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just realized this probably doesn't work with subclasses / method redefinitions, but don't have time right now to look into it.