Skip to content

Instantly share code, notes, and snippets.

@wycats
Created January 22, 2009 03:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wycats/50397 to your computer and use it in GitHub Desktop.
Save wycats/50397 to your computer and use it in GitHub Desktop.
module Callbacks
def self.included(klass)
klass.extend ClassMethods
end
def run_callbacks(kind, options = {})
send("_run_#{kind}_callbacks")
end
class Callback
@@_callback_sequence = 0
def initialize(filter, kind, options, klass, name)
@kind, @klass = kind, klass
@name = name
@filter = _compile_filter(filter)
@options = _compile_options(options)
end
# This will supply contents for before and around filters, and no
# contents for after filters (for the forward pass).
def start
# options[0] is the compiled form of supplied conditions
# options[1] is the "end" for the conditional
if @kind == :before || @kind == :around
if @kind == :before
# if condition # before_save :filter_name, :if => :condition
# filter_name
# end
[@options[0], @filter, @options[1]].compact.join("\n")
elsif @options[0]
# Compile around filters with conditions into proxy methods
# that contain the conditions.
#
# For `around_save :filter_name, :if => :condition':
#
# def _conditional_callback_save_17
# if condition
# filter_name do
# yield self
# end
# else
# yield self
# end
# end
name = "_conditional_callback_#{@kind}_#{@@_callback_sequence += 1}"
txt = <<-RUBY
def #{name}
#{@options[0]}
#{@filter} do
yield self
end
else
yield self
end
end
RUBY
@klass.class_eval(txt)
"#{name} do"
else
"#{@filter} do"
end
end
end
# This will supply contents for around and after filters, but not
# before filters (for the backward pass).
def end
if @kind == :around || @kind == :after
# if condition # after_save :filter_name, :if => :condition
# filter_name
# end
if @kind == :after
[@options[0], @filter, @options[1]].compact.join("\n")
else
"end"
end
end
end
private
# Options support the same options as filters themselves (and support
# symbols, string, procs, and objects), so compile a conditional
# expression based on the options
def _compile_options(options)
return [] unless options.key?(:if) || options.key?(:unless)
conditions = []
if options.key?(:if)
filter, yield_self = _compile_filter(options[:if])
conditions << filter
end
if options.key?(:unless)
filter, yield_self = _compile_filter(options[:unless])
conditions << "!#{filter}"
end
["if #{conditions.join(" && ")}", "end"]
end
# Filters support:
# Symbols:: A method to call
# Strings:: Some content to evaluate
# Procs:: A proc to call with the object
# Objects:: An object with a before_foo method on it to call
#
# All of these objects are compiled into methods and handled
# the same after this point:
# Symbols:: Already methods
# Strings:: class_eval'ed into methods
# Procs:: define_method'ed into methods
# Objects::
# a method is created that calls the before_foo method
# on the object.
def _compile_filter(filter)
case filter
when Symbol
filter
when Proc
method_name = "_callback_#{@kind}_#{@@_callback_sequence += 1}"
@klass.send(:define_method, method_name, &filter)
method_name << (filter.arity == 1 ? "(self)" : "")
when String
method_name = "_callback_#{@kind}_#{@@_callback_sequence += 1}"
@klass.class_eval <<-RUBY
def #{method_name}
#{filter}
end
RUBY
method_name
else
method_name = "_callback_#{@kind}_#{@@_callback_sequence += 1}"
kind, name = @kind, @name
@klass.send(:define_method, method_name) do
filter.send("#{kind}_#{name}", self)
end
method_name
end
end
end
# An Array with a compile method
class CallbackChain < Array
def compile
method = []
each do |callback|
method << callback.start
end
method << "yield self"
reverse_each do |callback|
method << callback.end
end
method.compact.join("\n")
end
end
module ClassMethods
CHAINS = {:before => :before, :around => :before, :after => :after}
# Make the _run_save_callbacks method. The generated method takes
# a block that it'll yield to. It'll call the before and around filters
# in order, yield the block, and then run the after filters.
#
# _run_save_callbacks do
# save
# end
def _define_runner(symbol, str, options)
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{symbol}_callbacks
#{str}
end
RUBY
before_name, around_name, after_name =
options.values_at(:before, :after, :around)
end
# Define callbacks.
#
# Creates a <name>_callback method that you can use to add callbacks.
#
# Syntax:
# save_callback :before, :before_meth
# save_callback :after, :after_meth, :if => :condition
# save_callback :around {|r| stuff; yield; stuff }
#
# The <name>_callback method also updates the _run_<name>_callbacks
# method, which is the public API to run the callbacks.
def define_callbacks(*symbols)
symbols.each do |symbol|
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
class_inheritable_accessor :_#{symbol}_callbacks
self._#{symbol}_callbacks = CallbackChain.new
def self.#{symbol}_callback(type, *filters, &blk)
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(blk) if block_given?
filters.map! {|f| Callback.new(f, type, options, self, :#{symbol})}
self._#{symbol}_callbacks.push(*filters)
_define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, options)
end
#{symbol}_callback(:before)
RUBY
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment