lib/couchrest/mixins/callbacks.rb in norr-couchrest-0.33.02 vs lib/couchrest/mixins/callbacks.rb in norr-couchrest-0.33.06
- old
+ new
@@ -1,11 +1,32 @@
+# Copyright (c) 2006-2009 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
+# http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
+# http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
+
require File.join(File.dirname(__FILE__), '..', 'support', 'class')
-# Extracted from ActiveSupport::Callbacks written by Yehuda Katz
-# http://github.com/wycats/rails/raw/abstract_controller/activesupport/lib/active_support/new_callbacks.rb
-# http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
-
module CouchRest
# 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.
@@ -83,34 +104,33 @@
# saved
module Callbacks
def self.included(klass)
klass.extend ClassMethods
end
-
+
def run_callbacks(kind, options = {}, &blk)
send("_run_#{kind}_callbacks", &blk)
end
-
+
class Callback
@@_callback_sequence = 0
-
+
attr_accessor :filter, :kind, :name, :options, :per_key, :klass
- def initialize(filter, kind, options, klass, name)
+ def initialize(filter, kind, options, klass)
@kind, @klass = kind, klass
- @name = name
-
+
normalize_options!(options)
@per_key = options.delete(:per_key)
@raw_filter, @options = filter, options
@filter = _compile_filter(filter)
@compiled_options = _compile_options(options)
@callback_id = next_id
_compile_per_key_options
end
-
+
def clone(klass)
obj = super()
obj.klass = klass
obj.per_key = @per_key.dup
obj.options = @options.dup
@@ -118,39 +138,38 @@
obj.per_key[:unless] = @per_key[:unless].dup
obj.options[:if] = @options[:if].dup
obj.options[:unless] = @options[:unless].dup
obj
end
-
+
def normalize_options!(options)
- options[:if] = Array(options[:if])
- options[:unless] = Array(options[:unless])
+ options[:if] = Array.wrap(options[:if])
+ options[:unless] = Array.wrap(options[:unless])
options[:per_key] ||= {}
- options[:per_key][:if] = Array(options[:per_key][:if])
- options[:per_key][:unless] = Array(options[:per_key][:unless])
+ options[:per_key][:if] = Array.wrap(options[:per_key][:if])
+ options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
end
-
+
def next_id
@@_callback_sequence += 1
end
-
- def matches?(_kind, _name, _filter)
+
+ def matches?(_kind, _filter)
@kind == _kind &&
- @name == _name &&
@filter == _filter
end
def _update_filter(filter_options, new_options)
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
end
-
+
def recompile!(_options, _per_key)
_update_filter(self.options, _options)
_update_filter(self.per_key, _per_key)
-
+
@callback_id = next_id
@filter = _compile_filter(@raw_filter)
@compiled_options = _compile_options(@options)
_compile_per_key_options
end
@@ -162,34 +181,35 @@
def _one_time_conditions_valid_#{@callback_id}?
true #{key_options[0]}
end
RUBY_EVAL
end
-
+
# This will supply contents for before and around filters, and no
# contents for after filters (for the forward pass).
def start(key = nil, options = {})
object, terminator = (options || {}).values_at(:object, :terminator)
-
+
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
-
+
terminator ||= false
-
+
# 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
filter = <<-RUBY_EVAL
unless halted
result = #{@filter}
- halted ||= (#{terminator})
+ halted = (#{terminator})
end
RUBY_EVAL
+
[@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
else
# Compile around filters with conditions into proxy methods
# that contain the conditions.
#
@@ -202,36 +222,36 @@
# end
# else
# yield self
# end
# end
-
+
name = "_conditional_callback_#{@kind}_#{next_id}"
- txt = <<-RUBY_EVAL
+ txt, line = <<-RUBY_EVAL, __LINE__ + 1
def #{name}(halted)
#{@compiled_options[0] || "if true"} && !halted
#{@filter} do
yield self
end
else
yield self
end
end
RUBY_EVAL
- @klass.class_eval(txt)
+ @klass.class_eval(txt, __FILE__, line)
"#{name}(halted) do"
end
end
end
-
+
# This will supply contents for around and after filters, but not
# before filters (for the backward pass).
def end(key = nil, options = {})
object = (options || {})[:object]
-
+
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
-
+
if @kind == :around || @kind == :after
# if condition # after_save :filter_name, :if => :condition
# filter_name
# end
if @kind == :after
@@ -239,31 +259,31 @@
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 [] if options[:if].empty? && options[:unless].empty?
-
+
conditions = []
-
+
unless options[:if].empty?
- conditions << Array(_compile_filter(options[:if]))
+ conditions << Array.wrap(_compile_filter(options[:if]))
end
-
+
unless options[:unless].empty?
- conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
+ conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
end
-
+
["if #{conditions.flatten.join(" && ")}", "end"]
end
-
+
# Filters support:
# Arrays:: Used in conditions. This is used to specify
# multiple conditions. Used internally to
# merge conditions from skip_* filters
# Symbols:: A method to call
@@ -285,67 +305,76 @@
case filter
when Array
filter.map {|f| _compile_filter(f)}
when Symbol
filter
+ when String
+ "(#{filter})"
when Proc
@klass.send(:define_method, method_name, &filter)
- method_name << (filter.arity == 1 ? "(self)" : "")
- when String
- @klass.class_eval <<-RUBY_EVAL
- def #{method_name}
- #{filter}
+ return method_name if filter.arity == 0
+
+ method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
+ else
+ @klass.send(:define_method, "#{method_name}_object") { filter }
+
+ _normalize_legacy_filter(kind, filter)
+
+ @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def #{method_name}(&blk)
+ #{method_name}_object.send(:#{kind}, self, &blk)
end
RUBY_EVAL
+
method_name
- else
- kind, name = @kind, @name
- @klass.send(:define_method, method_name) do
- filter.send("#{kind}_#{name}", self)
- end
- method_name
end
end
- end
- # This method_missing is supplied to catch callbacks with keys and create
- # the appropriate callback for future use.
- def method_missing(meth, *args, &blk)
- if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/
- return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
+ def _normalize_legacy_filter(kind, filter)
+ if !filter.respond_to?(kind) && filter.respond_to?(:filter)
+ filter.class_eval(
+ "def #{kind}(context, &block) filter(context, &block) end",
+ __FILE__, __LINE__ - 1)
+ elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
+ def filter.around(context)
+ should_continue = before(context)
+ yield if should_continue
+ after(context)
+ end
+ end
end
- super
+
end
-
+
# An Array with a compile method
class CallbackChain < Array
def initialize(symbol)
@symbol = symbol
end
-
+
def compile(key = nil, options = {})
method = []
method << "halted = false"
each do |callback|
method << callback.start(key, options)
end
- method << "yield self if block_given?"
+ method << "yield self if block_given? && !halted"
reverse_each do |callback|
method << callback.end(key, options)
end
method.compact.join("\n")
end
-
+
def clone(klass)
chain = CallbackChain.new(@symbol)
chain.push(*map {|c| c.clone(klass)})
end
end
-
+
module ClassMethods
- CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS")
-
+ #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
@@ -353,47 +382,49 @@
# end
#
# The _run_save_callbacks method can optionally take a key, which
# will be used to compile an optimized callback method for each
# key. See #define_callbacks for more information.
- def _define_runner(symbol, str, options)
- str = <<-RUBY_EVAL
- def _run_#{symbol}_callbacks(key = nil)
+ def _define_runner(symbol)
+ body = send("_#{symbol}_callback").
+ compile(nil, :terminator => send("_#{symbol}_terminator"))
+
+ body, line = <<-RUBY_EVAL, __LINE__ + 1
+ def _run_#{symbol}_callbacks(key = nil, &blk)
if key
- send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? }
+ name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
+
+ unless respond_to?(name)
+ self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
+ end
+
+ send(name, &blk)
else
- #{str}
+ #{body}
end
end
RUBY_EVAL
-
- class_eval str, __FILE__, __LINE__ + 1
-
- before_name, around_name, after_name =
- options.values_at(:before, :after, :around)
+
+ undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
+ class_eval body, __FILE__, line
end
-
+
# This is called the first time a callback is called with a particular
# key. It creates a new callback method for the key, calculating
# which callbacks can be omitted because of per_key conditions.
- def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
+ def _create_keyed_callback(name, kind, obj, &blk)
@_keyed_callbacks ||= {}
- @_keyed_callbacks[[kind, key]] ||= begin
- str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator"))
+ @_keyed_callbacks[name] ||= begin
+ str = send("_#{kind}_callback").
+ compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
- self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks
- #{str}
- end
- RUBY_EVAL
-
+ class_eval "def #{name}() #{str} end", __FILE__, __LINE__
+
true
end
-
- obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
end
-
+
# Define callbacks.
#
# Creates a <name>_callback method that you can use to add callbacks.
#
# Syntax:
@@ -421,62 +452,80 @@
# run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
#
# In that case, each action_name would get its own compiled callback
# method that took into consideration the per_key conditions. This
# is a speed improvement for ActionPack.
+ def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
+ type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
+ options = filters.last.is_a?(Hash) ? filters.pop : {}
+ filters.unshift(block) if block
+
+ callbacks = send("_#{name}_callback")
+ yield callbacks, type, filters, options if block_given?
+
+ _define_runner(name)
+ end
+
+ alias_method :_reset_callbacks, :_update_callbacks
+
+ def set_callback(name, *filters, &block)
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
+ filters.map! do |filter|
+ # overrides parent class
+ callbacks.delete_if {|c| c.matches?(type, filter) }
+ Callback.new(filter, type, options.dup, self)
+ end
+
+ options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
+ end
+ end
+
+ def skip_callback(name, *filters, &block)
+ _update_callbacks(name, filters, block) do |callbacks, type, filters, options|
+ filters.each do |filter|
+ callbacks = send("_#{name}_callback=", callbacks.clone(self))
+
+ filter = callbacks.find {|c| c.matches?(type, filter) }
+
+ if filter && options.any?
+ filter.recompile!(options, options[:per_key] || {})
+ else
+ callbacks.delete(filter)
+ end
+ end
+ end
+ end
+
def define_callbacks(*symbols)
terminator = symbols.pop if symbols.last.is_a?(String)
symbols.each do |symbol|
- self.extlib_inheritable_accessor("_#{symbol}_terminator")
- self.send("_#{symbol}_terminator=", terminator)
- self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- extlib_inheritable_accessor :_#{symbol}_callbacks
- self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
+ extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
- def self.#{symbol}_callback(*filters, &blk)
- type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
- options = filters.last.is_a?(Hash) ? filters.pop : {}
- filters.unshift(blk) if block_given?
-
- filters.map! do |filter|
- # overrides parent class
- self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)}
- Callback.new(filter, type, options.dup, self, :#{symbol})
- end
- self._#{symbol}_callbacks.push(*filters)
- _define_runner(:#{symbol},
- self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
- options)
- end
-
- def self.skip_#{symbol}_callback(*filters, &blk)
- type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
- options = filters.last.is_a?(Hash) ? filters.pop : {}
- filters.unshift(blk) if block_given?
- filters.each do |filter|
- self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
-
- filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
- per_key = options[:per_key] || {}
- if filter
- filter.recompile!(options, per_key)
- else
- self._#{symbol}_callbacks.delete(filter)
+ extlib_inheritable_accessor("_#{symbol}_callback") do
+ CallbackChain.new(symbol)
+ end
+
+ _define_runner(symbol)
+
+ # Define more convenient callback methods
+ # set_callback(:save, :before) becomes before_save
+ [:before, :after, :around].each do |filter|
+ self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def self.#{filter}_#{symbol}(*symbols, &blk)
+ _alias_callbacks(symbols, blk) do |callback, options|
+ set_callback(:#{symbol}, :#{filter}, callback, options)
end
- _define_runner(:#{symbol},
- self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
- options)
end
-
- end
-
- def self.reset_#{symbol}_callbacks
- self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
- _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {})
- end
-
- self.#{symbol}_callback(:before)
- RUBY_EVAL
+ RUBY_EVAL
+ end
+ end
+ end
+
+ def _alias_callbacks(callbacks, block)
+ options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
+ callbacks.push(block) if block
+ callbacks.each do |callback|
+ yield callback, options
end
end
end
end
end