diff options
-rw-r--r-- | activemodel/test/cases/validations_test.rb | 2 | ||||
-rw-r--r-- | activerecord/test/cases/callbacks_test.rb | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support/callbacks.rb | 413 |
3 files changed, 193 insertions, 224 deletions
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 3241a03b53..3e84297cc2 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -166,7 +166,7 @@ class ValidationsTest < ActiveModel::TestCase def test_invalid_validator Topic.validate :i_dont_exist - assert_raise(NameError) do + assert_raises(NoMethodError) do t = Topic.new t.valid? end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 187cad9599..c8f56e3c73 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -43,7 +43,7 @@ class CallbackDeveloper < ActiveRecord::Base end class CallbackDeveloperWithFalseValidation < CallbackDeveloper - before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false } + before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index a151aa03b5..e733257b67 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -76,8 +76,9 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - runner_name = self.class.__define_callbacks(kind, self) - send(runner_name, &block) + runner = send("_#{kind}_callbacks").compile + e = Filters::Environment.new(self, false, nil, block) + runner.call(e).value end private @@ -88,44 +89,32 @@ module ActiveSupport def halted_callback_hook(filter) end - class Callback #:nodoc:# - @@_callback_sequence = 0 - - class Basic < Callback - end + module Filters + Environment = Struct.new(:target, :halted, :value, :run_block) + end - class Object < Callback - def duplicates?(other) - false - end + class Callback #:nodoc:# + def self.build(chain, filter, kind, options) + new chain.name, filter, kind, options, chain.config end - def self.build(chain, filter, kind, options, _klass) - klass = case filter - when Array, Symbol, String - Callback::Basic - else - Callback::Object - end - klass.new chain, filter, kind, options, _klass - end + attr_accessor :kind, :options, :name + attr_reader :chain_config - attr_accessor :chain, :kind, :options, :klass, :raw_filter + def initialize(name, filter, kind, options, chain_config) + @chain_config = chain_config + @name = name + @kind = kind + @filter = filter + @options = options + @key = compute_identifier filter - def initialize(chain, filter, kind, options, klass) - @chain, @kind, @klass = chain, kind, klass deprecate_per_key_option(options) normalize_options!(options) - - @raw_filter, @options = filter, options - @key = compute_identifier filter - @source = _compile_source(filter) - recompile_options! end - def filter - @key - end + def filter; @key; end + def raw_filter; @filter; end def deprecate_per_key_option(options) if options[:per_key] @@ -133,14 +122,18 @@ module ActiveSupport end end - def clone(chain, klass) - obj = super() - obj.chain = chain - obj.klass = klass - obj.options = @options.dup - obj.options[:if] = @options[:if].dup - obj.options[:unless] = @options[:unless].dup - obj + def merge(chain, new_options) + _options = { + :if => @options[:if].dup, + :unless => @options[:unless].dup + } + + deprecate_per_key_option new_options + + _options[:if].concat Array(new_options.fetch(:unless, [])) + _options[:unless].concat Array(new_options.fetch(:if, [])) + + self.class.build chain, @filter, @kind, _options end def normalize_options!(options) @@ -148,137 +141,90 @@ module ActiveSupport options[:unless] = Array(options[:unless]) end - def name - chain.name - end - - def next_id - @@_callback_sequence += 1 - end - def matches?(_kind, _filter) @kind == _kind && filter == _filter end def duplicates?(other) - return false unless self.class == other.class - - matches?(other.kind, other.filter) - end - - def _update_filter(filter_options, new_options) - filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless) - filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if) - end - - def recompile!(_options) - deprecate_per_key_option(_options) - _update_filter(self.options, _options) - - recompile_options! + case @filter + when Symbol, String + matches?(other.kind, other.filter) + else + false + end end # Wraps code with filter - def apply(code) - case @kind + def apply(next_callback) + user_conditions = conditions_lambdas + user_callback = make_lambda @filter + + case kind when :before - <<-RUBY_EVAL - if !halted && #{@compiled_options} - # This double assignment is to prevent warnings in 1.9.3 as - # the `result` variable is not always used except if the - # terminator code refers to it. - result = result = #{@source} - halted = (#{chain.config[:terminator]}) - if halted - halted_callback_hook(#{@raw_filter.inspect.inspect}) + halted_lambda = eval "lambda { |result| #{chain_config[:terminator]} }" + lambda { |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + result = user_callback.call target, value + env.halted = halted_lambda.call result + if env.halted + target.send :halted_callback_hook, @filter.inspect end end - #{code} - RUBY_EVAL + next_callback.call env + } when :after - <<-RUBY_EVAL - #{code} - if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} - #{@source} + if chain_config[:skip_after_callbacks_if_terminated] + lambda { |env| + env = next_callback.call env + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + env + } + else + lambda { |env| + env = next_callback.call env + target = env.target + value = env.value + halted = env.halted + + if user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + env + } end - RUBY_EVAL when :around - name = define_conditional_callback - <<-RUBY_EVAL - #{name}(halted) do - #{code} - value - end - RUBY_EVAL + lambda { |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call(target, value) { + env = next_callback.call env + env.value + } + env + else + next_callback.call env + end + } end end private - def compute_identifier(filter) - case filter - when String, ::Proc - filter.object_id - else - filter - end - end - - # Compile around filters with conditions into proxy methods - # that contain the conditions. - # - # For `set_callback :save, :around, :filter_name, if: :condition`: - # - # def _conditional_callback_save_17 - # if condition - # filter_name do - # yield self - # end - # else - # yield self - # end - # end - def define_conditional_callback - name = "_conditional_callback_#{@kind}_#{next_id}" - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}(halted) - if #{@compiled_options} && !halted - #{@source} do - yield self - end - else - yield self - end - end - RUBY_EVAL - name - end - - # 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 recompile_options! - conditions = ["true"] - - unless options[:if].empty? - conditions << Array(_compile_source(options[:if])) - end - - unless options[:unless].empty? - conditions << Array(_compile_source(options[:unless])).map {|f| "!#{f}"} - end - - @compiled_options = conditions.flatten.join(" && ") - end - - def _method_name_for_object_filter(kind, filter, append_next_id = true) - class_name = filter.kind_of?(Class) ? filter.to_s : filter.class.to_s - class_name.gsub!(/<|>|#/, '') - class_name.gsub!(/\/|:/, "_") - - method_name = "_callback_#{kind}_#{class_name}" - method_name << "_#{next_id}" if append_next_id - method_name + def invert_lambda(l) + lambda { |*args, &blk| !l.call(*args, &blk) } end # Filters support: @@ -301,38 +247,61 @@ module ActiveSupport # Objects:: # a method is created that calls the before_foo method # on the object. - def _compile_source(filter) + def make_lambda(filter) case filter - when Array - filter.map {|f| _compile_source(f)} when Symbol - filter + lambda { |target, value, &blk| target.send filter, &blk } when String - "(#{filter})" + l = eval "lambda { |value| #{filter} }" + lambda { |target,value| target.instance_exec(value, &l) } when ::Proc - method_name = "_callback_#{@kind}_#{next_id}" - @klass.send(:define_method, method_name, &filter) - return method_name if filter.arity <= 0 + if filter.arity <= 0 + return lambda { |target, _| target.instance_exec(&filter) } + end - method_name << (filter.arity == 1 ? "(self)" : "(self, ::Proc.new)") + if filter.arity == 1 + lambda { |target, _| + target.instance_exec(target, &filter) + } + else + lambda { |target, _| + target.instance_exec target, ::Proc.new, &filter + } + end else - method_name = _method_name_for_object_filter(kind, filter) - @klass.send(:define_method, "#{method_name}_object") { filter } - - _normalize_legacy_filter(kind, filter) - scopes = Array(chain.config[:scope]) + scopes = Array(chain_config[:scope]) method_to_call = scopes.map{ |s| public_send(s) }.join("_") - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{method_name}(&blk) - #{method_name}_object.send(:#{method_to_call}, self, &blk) - end - RUBY_EVAL + lambda { |target, _, &blk| + filter.public_send method_to_call, target, &blk + } + end + end - method_name + def compute_identifier(filter) + case filter + when String, ::Proc + filter.object_id + else + filter end end + def conditions_lambdas + conditions = [] + + unless options[:if].empty? + lambdas = Array(options[:if]).map { |c| make_lambda c } + conditions.concat lambdas + end + + unless options[:unless].empty? + lambdas = Array(options[:unless]).map { |c| make_lambda c } + conditions.concat lambdas.map { |l| invert_lambda l } + end + conditions + end + def _normalize_legacy_filter(kind, filter) if !filter.respond_to?(kind) && filter.respond_to?(:filter) message = "Filter object with #filter method is deprecated. Define method corresponding " \ @@ -354,7 +323,9 @@ module ActiveSupport end # An Array with a compile method. - class CallbackChain < Array #:nodoc:# + class CallbackChain #:nodoc:# + include Enumerable + attr_reader :name, :config def initialize(name, config) @@ -363,18 +334,47 @@ module ActiveSupport :terminator => "false", :scope => [ :kind ] }.merge!(config) + @chain = [] + @callbacks = nil + end + + def each(&block); @chain.each(&block); end + def index(o); @chain.index(o); end + def empty?; @chain.empty?; end + + def insert(index, o) + @callbacks = nil + @chain.insert(index, o) + end + + def delete(o) + @callbacks = nil + @chain.delete(o) + end + + def clear + @callbacks = nil + @chain.clear + self + end + + def initialize_copy(other) + @callbacks = nil + @chain = other.chain.dup end def compile - method = ["value = nil", "halted = false"] - callbacks = "value = !halted && (!block_given? || yield)" - reverse_each do |callback| - callbacks = callback.apply(callbacks) + return @callbacks if @callbacks + + @callbacks = lambda { |env| + block = env.run_block + env.value = !env.halted && (!block || block.call) + env + } + @chain.reverse_each do |callback| + @callbacks = callback.apply(@callbacks) end - method << callbacks - - method << "value" - method.join("\n") + @callbacks end def append(*callbacks) @@ -385,58 +385,32 @@ module ActiveSupport callbacks.each { |c| prepend_one(c) } end + protected + def chain; @chain; end + private def append_one(callback) + @callbacks = nil remove_duplicates(callback) - push(callback) + @chain.push(callback) end def prepend_one(callback) + @callbacks = nil remove_duplicates(callback) - unshift(callback) + @chain.unshift(callback) end def remove_duplicates(callback) - delete_if { |c| callback.duplicates?(c) } + @callbacks = nil + @chain.delete_if { |c| callback.duplicates?(c) } end end module ClassMethods - # This method defines callback chain method for the given kind - # if it was not yet defined. - # This generated method plays caching role. - def __define_callbacks(kind, object) #:nodoc: - name = __callback_runner_name(kind) - unless object.respond_to?(name, true) - str = object.send("_#{kind}_callbacks").compile - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}() #{str} end - protected :#{name} - RUBY_EVAL - end - name - end - - def __reset_runner(symbol) - name = __callback_runner_name(symbol) - undef_method(name) if method_defined?(name) - end - - def __callback_runner_name_cache - @__callback_runner_name_cache ||= ThreadSafe::Cache.new {|cache, kind| cache[kind] = __generate_callback_runner_name(kind) } - end - - def __generate_callback_runner_name(kind) - "_run__#{self.name.hash.abs}__#{kind}__callbacks" - end - - def __callback_runner_name(kind) - __callback_runner_name_cache[kind] - end - # This is used internally to append, prepend and skip callbacks to the # CallbackChain. def __update_callbacks(name, filters = [], block = nil) #:nodoc: @@ -447,7 +421,6 @@ module ActiveSupport ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target| chain = target.send("_#{name}_callbacks") yield target, chain.dup, type, filters, options - target.__reset_runner(name) end end @@ -491,7 +464,7 @@ module ActiveSupport __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| mapped ||= filters.map do |filter| - Callback.build(chain, filter, type, options.dup, self) + Callback.build(chain, filter, type, options.dup) end options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) @@ -513,9 +486,8 @@ module ActiveSupport filter = chain.find {|c| c.matches?(type, filter) } if filter && options.any? - new_filter = filter.clone(chain, self) + new_filter = filter.merge(chain, options) chain.insert(chain.index(filter), new_filter) - new_filter.recompile!(options) end chain.delete(filter) @@ -532,12 +504,9 @@ module ActiveSupport chain = target.send("_#{symbol}_callbacks").dup callbacks.each { |c| chain.delete(c) } target.send("_#{symbol}_callbacks=", chain) - target.__reset_runner(symbol) end self.send("_#{symbol}_callbacks=", callbacks.dup.clear) - - __reset_runner(symbol) end # Define sets of events in the object lifecycle that support callbacks. |