Skip to content

Instantly share code, notes, and snippets.

@jodosha
Created February 6, 2009 21:58
Show Gist options
  • Save jodosha/59642 to your computer and use it in GitHub Desktop.
Save jodosha/59642 to your computer and use it in GitHub Desktop.
Lightweight implementation of ActiveSupport::Callbacks (no backward compatibilities)
ruby 1.8.6 (2008-03-03 patchlevel 114) [universal-darwin9.0]
test | old | new | boost
SymbolCallbackPerson | 0.418285131454468 | 0.198039770126343 | 2.11x
ProcCallbackPerson | 0.478709936141968 | 0.309146881103516 | 1.55x
MethodCallbackPerson | 0.298830032348633 | 0.114556074142456 | 2.60x
StringCallbackPerson | 0.782155990600586 | 0.653557062149048 | 1.97x
ObjectCallbackPerson | 0.49412202835083 | 0.30273699760437 | 1.63x
ConditionalPerson | 5.81794190406799 | 5.45951700210571 | 1.07x
ruby 1.9.1p0 (2009-01-30 revision 21907) [i386-darwin9.5.0]
test | old | new | boost
SymbolCallbackPerson | 0.291076898574829 | 0.105682849884033 | 2.75x
ProcCallbackPerson | 0.308841228485107 | 0.11469292640686 | 2.70x
MethodCallbackPerson | 0.298770904541016 | 0.11210298538208 | 2.67x
StringCallbackPerson | 0.750032901763916 | 0.560175895690918 | 1.34x
ObjectCallbackPerson | 0.331979036331177 | 0.132863998413086 | 2.50x
ConditionalPerson | 4.04803109169006 | 3.75437092781067 | 1.08x
jruby 1.1.6 (ruby 1.8.6 patchlevel 114) (2008-12-17 rev 8388) [i386-java]
test | old | new | boost
SymbolCallbackPerson | 0.813068151473999 | 0.48386716842651367 | 1.60x
ProcCallbackPerson | 0.5594620704650879 | 0.3466830253601074 | 1.61x
MethodCallbackPerson | 0.5431900024414062 | 0.31347084045410156 | 1.73x
StringCallbackPerson | 1.5273478031158447 | 1.314432144165039 | 1.16x
ObjectCallbackPerson | 0.6658079624176025 | 0.44325995445251465 | 1.50x
ConditionalPerson | 7.16249680519104 | 6.2471020221710205 | 1.15x
module ActiveSupport
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
# before or after an alteration of the object state.
#
# Mixing in this module allows you to define callbacks in your class.
#
# Example:
# class Storage
# include ActiveSupport::Callbacks
#
# define_callbacks :before_save, :after_save
# end
#
# class ConfigStorage < Storage
# before_save :saving_message
# def saving_message
# puts "saving..."
# end
#
# after_save do |object|
# puts "saved"
# end
#
# def save
# run_callbacks(:before_save)
# puts "- save"
# run_callbacks(:after_save)
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# saving...
# - save
# saved
#
# Callbacks from parent classes are inherited.
#
# Example:
# class Storage
# include ActiveSupport::Callbacks
#
# define_callbacks :before_save, :after_save
#
# before_save :prepare
# def prepare
# puts "preparing save"
# end
# end
#
# class ConfigStorage < Storage
# before_save :saving_message
# def saving_message
# puts "saving..."
# end
#
# after_save do |object|
# puts "saved"
# end
#
# def save
# run_callbacks(:before_save)
# puts "- save"
# run_callbacks(:after_save)
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# preparing save
# saving...
# - save
# saved
module Callbacks
def self.included(recipient)
recipient.extend ClassMethods
recipient.send :include, InstanceMethods
end
module ClassMethods
def define_callbacks(*callbacks)
callbacks.each do |callback|
class_eval <<-END, __FILE__, __LINE__ + 1
def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block)
conditions = methods.extract_options! # conditions = methods.extract_options!
#{callback}_callback_chain.push(methods, conditions) unless methods.empty? # before_save_callback_chain.push(methods, conditions) unless methods.empty?
#{callback}_callback_chain.push(block, conditions) if block_given? # before_save_callback_chain_callback_chain.push(block, conditions) if block_given?
end # end
#
def self.#{callback}_callback_chain # def self.before_save_callback_chain
@#{callback}_callback_chain ||= CallbackChain.new('#{callback}') # @before_save_callback_chain ||= CallbackChain.new('before_save')
end # end
END
end
end
end
module InstanceMethods
# Runs all the callbacks defined for the given options.
#
# If a block is given it will be called after each callback receiving as arguments:
#
# * the result from the callback
# * the object which has the callback
#
# If the result from the block evaluates to false, the callback chain is stopped.
#
# Example:
# class Storage
# include ActiveSupport::Callbacks
#
# define_callbacks :before_save, :after_save
# end
#
# class ConfigStorage < Storage
# before_save :pass
# before_save :pass
# before_save :stop
# before_save :pass
#
# def pass
# puts "pass"
# end
#
# def stop
# puts "stop"
# return false
# end
#
# def save
# result = run_callbacks(:before_save) { |result, object| result == false }
# puts "- save" if result
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# pass
# pass
# stop
def run_callbacks(callback, options = {}, &block)
self.class.send("#{callback}_callback_chain").run(self, options, &block)
end
end
class CallbackChain < OrderedHash
def initialize(kind, *arguments, &block)
@kind = kind
super(arguments, &block)
end
def run(object, options = {}, &terminator)
enumerator = options[:enumerator] || :each
unless block_given?
send(enumerator) { |callback, conditions| run_callback(callback, object, conditions) }
else
send(enumerator) do |callback, conditions|
result = run_callback(callback, object, conditions)
break result if terminator.call(result, object)
end
end
end
def find(callback, &block)
callbacks.select { |c| c == callback && (!block_given? || yield(c)) }.first
end
def delete(callback)
super find(callback)
end
def push(callbacks, conditions = {})
[callbacks].flatten.each { |callback| self[callback] = conditions }
end
alias_method :<<, :push
def reverse_each(&block)
callbacks.reverse.each { |callback| yield callback, self[callback] }
end
def callbacks
@keys
end
def size
callbacks.size
end
protected
def evaluate_method(callback, object)
case callback
when Symbol
object.send callback
when Proc, Method
callback.call object
when String
eval(callback, object.instance_eval { binding })
else
if callback.respond_to? @kind
callback.send(@kind, object)
else
raise ArgumentError,
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
"a block to be invoked, or an object responding to the callback method."
end
end
end
def run_callback(callback, object, conditions)
evaluate_method(callback, object) if run_callback?(object, conditions)
end
def run_callback?(object, conditions)
return true if conditions.empty?
[conditions[:if]].flatten.compact.all? { |c| evaluate_method(c, object) } &&
![conditions[:unless]].flatten.compact.any? { |c| evaluate_method(c, object) }
end
end
end
end
require 'benchmark'
$:.unshift "#{File.dirname(__FILE__)}/../lib"
require 'active_support'
class Record
include ActiveSupport::Callbacks
define_callbacks :before_save, :after_save
def history
@history ||= []
end
def save
run_callbacks(:before_save)
run_callbacks(:after_save)
end
end
class SymbolCallbackPerson < Record
before_save :validate_email
after_save :send_mail_notification
private
def validate_email
history << "validate_email"
end
def send_mail_notification
history << "send_mail_notification"
end
end
class ProcCallbackPerson < Record
before_save do |person|
person.history << "validate_email"
end
after_save do |person|
person.history << "send_mail_notification"
end
end
class MethodCallbackPerson < Record
class << self
def validate_email(person)
person.history << "validate_email"
end
def send_mail_notification(person)
person.history << "send_mail_notification"
end
end
end
class StringCallbackPerson < Record
before_save "history << %(validate_email)"
after_save "history << %(send_mail_notification)"
end
class EmailValidator
def before_save(person)
person.history << "validate_email"
end
end
class EmailNotificator
def after_save(person)
person.history << "send_mail_notification"
end
end
class ObjectCallbackPerson < Record
before_save EmailValidator.new
after_save EmailNotificator.new
end
class ConditionalPerson < Record
# proc
before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true }
before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false }
before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false }
before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true }
# symbol
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes
before_save Proc.new { |r| r.history << "b00m" }, :if => :no
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no
before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes
# string
before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes'
before_save Proc.new { |r| r.history << "b00m" }, :if => 'no'
before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no'
before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes'
# Array with conditions
before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :if => [:yes, :other_yes]
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, :no]
before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :unless => [:no, :other_no]
before_save Proc.new { |r| r.history << "b00m" }, :unless => [:yes, :no]
# Combined if and unless
before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no
before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes
# Array with different types of conditions
before_save Proc.new { |r| r.history << [:before_save, :symbol_proc_string_array] }, :if => [:yes, Proc.new { |r| true }, 'yes']
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no']
# Array with different types of conditions comibned if and unless
before_save Proc.new { |r| r.history << [:before_save, :combined_symbol_proc_string_array] },
:if => [:yes, Proc.new { |r| true }, 'yes'], :unless => [:no, 'no']
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no'], :unless => [:no, 'no']
def yes; true; end
def other_yes; true; end
def no; false; end
def other_no; false; end
end
def run(klass)
elapsed = Benchmark.realtime do
record = klass.new
10_000.times do
record.save
end
end
puts "#{klass} elapsed: #{elapsed}"
end
run SymbolCallbackPerson
run ProcCallbackPerson
run MethodCallbackPerson
run StringCallbackPerson
run ObjectCallbackPerson
run ConditionalPerson
require 'abstract_unit'
class Record
include ActiveSupport::Callbacks
define_callbacks :before_save, :after_save
class << self
def callback_symbol(callback_method)
returning("#{callback_method}_method") do |method_name|
define_method(method_name) do
history << [callback_method, :symbol]
end
end
end
def callback_string(callback_method)
"history << [#{callback_method.to_sym.inspect}, :string]"
end
def callback_proc(callback_method)
Proc.new { |model| model.history << [callback_method, :proc] }
end
def callback_object(callback_method)
klass = Class.new
klass.send(:define_method, callback_method) do |model|
model.history << [callback_method, :object]
end
klass.new
end
end
def history
@history ||= []
end
end
class Person < Record
[:before_save, :after_save].each do |callback_method|
callback_method_sym = callback_method.to_sym
send(callback_method, callback_symbol(callback_method_sym))
send(callback_method, callback_string(callback_method_sym))
send(callback_method, callback_proc(callback_method_sym))
send(callback_method, callback_object(callback_method_sym))
send(callback_method) { |model| model.history << [callback_method_sym, :block] }
end
def save
run_callbacks(:before_save)
run_callbacks(:after_save)
end
end
class ConditionalPerson < Record
# proc
before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true }
before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false }
before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false }
before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true }
# symbol
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes
before_save Proc.new { |r| r.history << "b00m" }, :if => :no
before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no
before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes
# string
before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes'
before_save Proc.new { |r| r.history << "b00m" }, :if => 'no'
before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no'
before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes'
# Array with conditions
before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :if => [:yes, :other_yes]
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, :no]
before_save Proc.new { |r| r.history << [:before_save, :symbol_array] }, :unless => [:no, :other_no]
before_save Proc.new { |r| r.history << "b00m" }, :unless => [:yes, :no]
# Combined if and unless
before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no
before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes
# Array with different types of conditions
before_save Proc.new { |r| r.history << [:before_save, :symbol_proc_string_array] }, :if => [:yes, Proc.new { |r| true }, 'yes']
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no']
# Array with different types of conditions comibned if and unless
before_save Proc.new { |r| r.history << [:before_save, :combined_symbol_proc_string_array] },
:if => [:yes, Proc.new { |r| true }, 'yes'], :unless => [:no, 'no']
before_save Proc.new { |r| r.history << "b00m" }, :if => [:yes, Proc.new { |r| true }, 'no'], :unless => [:no, 'no']
def yes; true; end
def other_yes; true; end
def no; false; end
def other_no; false; end
def save
run_callbacks(:before_save)
run_callbacks(:after_save)
end
end
class CallbacksTest < Test::Unit::TestCase
def test_save_person
person = Person.new
assert_equal [], person.history
person.save
assert_equal [
[:before_save, :symbol],
[:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :block],
[:after_save, :symbol],
[:after_save, :string],
[:after_save, :proc],
[:after_save, :object],
[:after_save, :block]
], person.history
end
end
class ConditionalCallbackTest < Test::Unit::TestCase
def test_save_conditional_person
person = ConditionalPerson.new
person.save
assert_equal [
[:before_save, :proc],
[:before_save, :proc],
[:before_save, :symbol],
[:before_save, :symbol],
[:before_save, :string],
[:before_save, :string],
[:before_save, :symbol_array],
[:before_save, :symbol_array],
[:before_save, :combined_symbol],
[:before_save, :symbol_proc_string_array],
[:before_save, :combined_symbol_proc_string_array]
], person.history
end
end
# class CallbackTest < Test::Unit::TestCase
# include ActiveSupport::Callbacks
#
# def test_eql
# callback = Callback.new(:before, :save, :identifier => :lifesaver)
# assert callback.eql?(Callback.new(:before, :save, :identifier => :lifesaver))
# assert callback.eql?(Callback.new(:before, :save))
# assert callback.eql?(:lifesaver)
# assert callback.eql?(:save)
# assert !callback.eql?(Callback.new(:before, :destroy))
# assert !callback.eql?(:destroy)
# end
#
# def test_dup
# a = Callback.new(:before, :save)
# assert_equal({}, a.options)
# b = a.dup
# b.options[:unless] = :pigs_fly
# assert_equal({:unless => :pigs_fly}, b.options)
# assert_equal({}, a.options)
# end
# end
class CallbackChainTest < Test::Unit::TestCase
include ActiveSupport::Callbacks
def setup
@chain = CallbackChain.new(:make)
@chain << [:bacon, :lettuce, :tomato]
end
def test_initialize
assert_equal 3, @chain.size
assert_equal [:bacon, :lettuce, :tomato], @chain.callbacks
end
def test_find
assert_equal :bacon, @chain.find(:bacon)
end
# def test_replace_or_append
# assert_equal [:bacon, :lettuce, :tomato], (@chain.replace_or_append!(Callback.new(:make, :bacon))).map(&:method)
# assert_equal [:bacon, :lettuce, :tomato, :turkey], (@chain.replace_or_append!(Callback.new(:make, :turkey))).map(&:method)
# assert_equal [:bacon, :lettuce, :tomato, :turkey, :mayo], (@chain.replace_or_append!(Callback.new(:make, :mayo))).map(&:method)
# end
def test_delete
assert_equal [:bacon, :lettuce, :tomato], @chain.callbacks
@chain.delete(:bacon)
assert_equal [:lettuce, :tomato], @chain.callbacks
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment