From c16c7a8de4e543a92de10a138bdd7caa5ac902d7 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 27 Feb 2009 19:25:45 -0800 Subject: Add support for callbacks --- activesupport/lib/active_support/new_callbacks.rb | 477 ++++++++++++++++++++++ 1 file changed, 477 insertions(+) create mode 100644 activesupport/lib/active_support/new_callbacks.rb (limited to 'activesupport/lib/active_support/new_callbacks.rb') diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb new file mode 100644 index 0000000000..cf717bfbb9 --- /dev/null +++ b/activesupport/lib/active_support/new_callbacks.rb @@ -0,0 +1,477 @@ +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 :save + # end + # + # class ConfigStorage < Storage + # save_callback :before, :saving_message + # def saving_message + # puts "saving..." + # end + # + # save_callback :after do |object| + # puts "saved" + # end + # + # def save + # _run_save_callbacks do + # puts "- save" + # end + # 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 :save + # + # save_callback :before, :prepare + # def prepare + # puts "preparing save" + # end + # end + # + # class ConfigStorage < Storage + # save_callback :before, :saving_message + # def saving_message + # puts "saving..." + # end + # + # save_callback :after do |object| + # puts "saved" + # end + # + # def save + # _run_save_callbacks do + # puts "- save" + # end + # end + # end + # + # config = ConfigStorage.new + # config.save + # + # Output: + # preparing save + # saving... + # - save + # saved + module NewCallbacks + 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) + @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 + obj.per_key[:if] = @per_key[:if].dup + 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[:per_key] ||= {} + options[:per_key][:if] = Array(options[:per_key][:if]) + options[:per_key][:unless] = Array(options[:per_key][:unless]) + end + + def next_id + @@_callback_sequence += 1 + end + + def matches?(_kind, _name, _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 + + def _compile_per_key_options + key_options = _compile_options(@per_key) + + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + 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}) + 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. + # + # 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}_#{next_id}" + txt = <<-RUBY_EVAL + 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) + "#{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 + [@compiled_options[0], @filter, @compiled_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 [] if options[:if].empty? && options[:unless].empty? + + conditions = [] + + unless options[:if].empty? + conditions << Array(_compile_filter(options[:if])) + end + + unless options[:unless].empty? + conditions << Array(_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 + # 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: + # Arrays:: Merged together into a single filter + # 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) + method_name = "_callback_#{@kind}_#{next_id}" + case filter + when Array + filter.map {|f| _compile_filter(f)} + when Symbol + 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} + 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) + 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?" + 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} + + # 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 + # + # 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) + if key + send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? } + else + #{str} + end + end + RUBY_EVAL + + class_eval str, __FILE__, __LINE__ + 1 + + before_name, around_name, after_name = + options.values_at(:before, :after, :around) + 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) + @_keyed_callbacks ||= {} + @_keyed_callbacks[[kind, key]] ||= begin + str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator")) + + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks + #{str} + end + RUBY_EVAL + + true + end + + obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk) + end + + # Define callbacks. + # + # Creates a _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 _callback method also updates the _run__callbacks + # method, which is the public API to run the callbacks. + # + # Also creates a skip__callback method that you can use to skip + # callbacks. + # + # When creating or skipping callbacks, you can specify conditions that + # are always the same for a given key. For instance, in ActionPack, + # we convert :only and :except conditions into per-key conditions. + # + # before_filter :authenticate, :except => "index" + # becomes + # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}} + # + # Per-Key conditions are evaluated only once per use of a given key. + # In the case of the above example, you would do: + # + # 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 define_callbacks(*symbols) + terminator = symbols.pop if symbols.last.is_a?(String) + symbols.each do |symbol| + self.class_inheritable_accessor("_#{symbol}_terminator") + self.send("_#{symbol}_terminator=", terminator) + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + class_inheritable_accessor :_#{symbol}_callbacks + self._#{symbol}_callbacks = CallbackChain.new(:#{symbol}) + + 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) + 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 + end + end + end + end +end -- cgit v1.2.3