Skip to content

Instantly share code, notes, and snippets.

@v2e4lisp
Last active August 29, 2015 14:00
Show Gist options
  • Select an option

  • Save v2e4lisp/11342428 to your computer and use it in GitHub Desktop.

Select an option

Save v2e4lisp/11342428 to your computer and use it in GitHub Desktop.
before/after filter
# Include this module
# to add :before and :after hooks for instance method
#
# Example
#
# class User
# include Hookable
#
# def initialize
# @name = "wenjun.yan"
# end
#
# def name
# @name
# end
#
# def name=(new_name)
# @name = new_name unless new_name == "Bob Marley"
# end
#
# before :name= do |new_name|
# puts "@name will be changed to #{new_name}"
# end
#
# after :name= do |new_name|
# if @name == new_name
# puts "@name has been changed to #{new_name}"
# else
# puts "fail to changing name to #{new_name}"
# end
# end
# end
#
# user = User.new
# user.name = "Larry Wall"
# user.name = "Yukihiro Matsumoto"
# user.name = "Bob Marley"
#
# # => @name will be changed to Larry Wall
# # => @name has been changed to Larry Wall
# # => @name will be changed to Yukihiro Matsumoto
# # => @name has been changed to Yukihiro Matsumoto
# # => @name will be changed to Bob Marley
# # => fail to changing name to Bob Marley
#
# # call method with hooks
# User.new.name
#
# # call method without hooks
# User.new.name_without_hooks
#
# # get all method hooks
# User.hooks
#
# # get all hooked methods
# User.hooks.keys
#
# 1. Hooks are executed in current instance context.
# 2. Method args and optional block are passed,
# so you can use them in the hook block
# 3. :after hooks do not change the return value of the original method
module Hookable
def self.included(base)
base.extend ClassMethods
base.class_eval {
undef_method :hooks if method_defined?(:hooks)
}
end
def run_hooks(before_or_after, method, *args)
self.class.hooks[method][before_or_after].each { |b|
instance_exec(*args, &b)
}
end
private :run_hooks
module ClassMethods
# Public: get all hooks
# hooks is a hash structured:
#
# {
# :method_name => {
# :before => [proc1, proc2 ... ]
# :after => [proc1, proc2 ... ]
# }
# ...
# }
def hooks
{}
end
protected
# Protected: add before hook
#
# method - {Symbol} method name
# block - code called before the method,
# block is evaluated in instance context.
#
# Example:
# before :some_method do
# puts "in before hook"
# end
def before(method, &block)
hook(:before, method, &block)
end
# Protected: add after hook
#
# method - {Symbol} method name
# block - code called after the method,
# block is evaluated in instance context.
#
# Example:
# after :some_method do
# puts "in after hook"
# end
def after(method, &block)
hook(:after, method, &block)
end
# method added trigger
# if the newly added method has any hook
# rename it and create a new method with the orignal method name,
# which will trigger the hooks.
def method_added(method)
# this method has hooks and has not been redefined
if hooks[method] and not method_defined? new_name_for(method)
redefine_method_with_hooks method
end
super
end
# Protected: Get a new name for method which is hooked
#
# method - name of a method with hooks
#
# Example:
#
# new_name_for(:change)
# # => change_without_hooks
#
# new_name_for(:changed?)
# # => changed_without_hooks?
#
# new_name_for(:change!)
# # => change_without_hooks!
#
# new_name_for(:name=)
# # => name_without_hooks=
#
# method name with a suffix "!", "?" or "=" will be renamed to
# method_without_hooks{suffix}.
# Other special method names(<<, [] ...) will not be handled
def new_name_for(method)
name = method.to_s
if ["!", "?", "="].include? name[-1]
"#{name[0..-2]}_without_hooks#{name[-1]}"
else
"#{name}_without_hooks"
end
end
def hook(before_or_after, method, &block)
old_method = new_name_for(method)
old_hooks = hooks.dup
old_hooks[method] ||= {:before => [], :after => []}
old_hooks[method][before_or_after] << block
singleton_class.class_eval {
define_method(:hooks) { old_hooks }
}
if method_defined?(method) and not method_defined?(old_method)
redefine_method_with_hooks method
end
end
def redefine_method_with_hooks(method)
old_method = new_name_for(method)
alias_method old_method, method
define_method(method) {|*args, &block|
args_with_optional_block = args + [block]
run_hooks(:before, method, *args_with_optional_block)
send(old_method, *args, &block).tap {
run_hooks(:after, method, *args_with_optional_block)
}
}
end
end
end
@v2e4lisp

Copy link
Copy Markdown
Author

継承にしたらどうなるか?まだ確認してないです。

@v2e4lisp

Copy link
Copy Markdown
Author

継承しても問題なし!
The hooks works like an attribute defined by class_attribute in rails.

rails class_attribute method

@v2e4lisp

Copy link
Copy Markdown
Author

E.G

require File.expand_path("../meta", __FILE__)

class User
  include Hookable

  def initialize
    @nickname = "wenjun.yan"
  end

  def nickname
    @nickname
  end

  def nickname=(new_nickname)
    @nickname = new_nickname unless new_nickname == "Bob Marley"
  end

  before :nickname do
  end

  before :nickname= do |new_nickname|
    puts "@nickname will be changed to #{new_nickname}"
  end

  after :nickname= do |new_nickname|
    if @nickname == new_nickname
      puts "@nickname has been changed to #{new_nickname}"
    else
      puts "fail to changing nickname to #{new_nickname}"
    end
  end
end

# p User.methods.map(&:to_s).grep /nickname/

class Me < User
  after :hi do
    puts "My nickname is #{nickname_without_hooks}"
  end

  def hi
    puts "hello!"
  end
end

p User.hooks
p Me.hooks

puts "-" * 50


user = User.new
user.nickname = "user"
user.nickname_without_hooks
user.nickname = "Larry Wall"
user.nickname = "Yukihiro Matsumoto"
user.nickname = "Bob Marley"

Me.new.hi

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