Created
January 22, 2009 03:10
-
-
Save wycats/50397 to your computer and use it in GitHub Desktop.
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 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