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