Created
February 6, 2009 21:58
-
-
Save jodosha/59642 to your computer and use it in GitHub Desktop.
Lightweight implementation of ActiveSupport::Callbacks (no backward compatibilities)
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
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 | |
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
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 |
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
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 |
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
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