diff options
-rw-r--r-- | activemodel/lib/active_model/callbacks.rb | 30 | ||||
-rw-r--r-- | activesupport/lib/active_support/callbacks.rb | 72 | ||||
-rw-r--r-- | activesupport/test/callbacks_test.rb | 75 |
3 files changed, 134 insertions, 43 deletions
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 16188aec6b..94d2181e4d 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -120,30 +120,24 @@ module ActiveModel private def _define_before_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.before_#{callback}(*args, &block) - set_callback(:#{callback}, :before, *args, &block) - end - CALLBACK + klass.define_singleton_method("before_#{callback}") do |*args, &block| + set_callback(:"#{callback}", :before, *args, &block) + end end def _define_around_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.around_#{callback}(*args, &block) - set_callback(:#{callback}, :around, *args, &block) - end - CALLBACK + klass.define_singleton_method("around_#{callback}") do |*args, &block| + set_callback(:"#{callback}", :around, *args, &block) + end end def _define_after_model_callback(klass, callback) #:nodoc: - klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1 - def self.after_#{callback}(*args, &block) - options = args.extract_options! - options[:prepend] = true - options[:if] = Array(options[:if]) << "value != false" - set_callback(:#{callback}, :after, *(args << options), &block) - end - CALLBACK + klass.define_singleton_method("after_#{callback}") do |*args, &block| + options = args.extract_options! + options[:prepend] = true + options[:if] = Array(options[:if]) << "value != false" + set_callback(:"#{callback}", :after, *(args << options), &block) + end end end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 1dcacf0b12..6fe7e0f4fb 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -91,7 +91,26 @@ module ActiveSupport class Callback #:nodoc:# @@_callback_sequence = 0 - attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter + class Basic < Callback + end + + class Object < Callback + def duplicates?(other) + false + end + 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 :chain, :kind, :options, :klass, :raw_filter def initialize(chain, filter, kind, options, klass) @chain, @kind, @klass = chain, kind, klass @@ -99,10 +118,15 @@ module ActiveSupport normalize_options!(options) @raw_filter, @options = filter, options - @filter = _compile_filter(filter) + @key = compute_identifier filter + @source = _compile_source(filter) recompile_options! end + def filter + @key + end + def deprecate_per_key_option(options) if options[:per_key] raise NotImplementedError, ":per_key option is no longer supported. Use generic :if and :unless options instead." @@ -133,16 +157,12 @@ module ActiveSupport end def matches?(_kind, _filter) - if @_is_object_filter && !_filter.is_a?(String) - _filter_matches = @filter.to_s.start_with?(_method_name_for_object_filter(_kind, _filter, false)) - else - _filter_matches = (@filter == _filter) - end - - @kind == _kind && _filter_matches + @kind == _kind && filter == _filter end def duplicates?(other) + return false unless self.class == other.class + matches?(other.kind, other.filter) end @@ -167,7 +187,7 @@ module ActiveSupport # 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 = #{@filter} + result = result = #{@source} halted = (#{chain.config[:terminator]}) if halted halted_callback_hook(#{@raw_filter.inspect.inspect}) @@ -179,7 +199,7 @@ module ActiveSupport <<-RUBY_EVAL #{code} if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} - #{@filter} + #{@source} end RUBY_EVAL when :around @@ -195,6 +215,15 @@ module ActiveSupport 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. # @@ -214,7 +243,7 @@ module ActiveSupport @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{name}(halted) if #{@compiled_options} && !halted - #{@filter} do + #{@source} do yield self end else @@ -232,11 +261,11 @@ module ActiveSupport conditions = ["true"] unless options[:if].empty? - conditions << Array(_compile_filter(options[:if])) + conditions << Array(_compile_source(options[:if])) end unless options[:unless].empty? - conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} + conditions << Array(_compile_source(options[:unless])).map {|f| "!#{f}"} end @compiled_options = conditions.flatten.join(" && ") @@ -272,30 +301,27 @@ module ActiveSupport # Objects:: # a method is created that calls the before_foo method # on the object. - def _compile_filter(filter) - @_is_object_filter = false - + def _compile_source(filter) case filter when Array - filter.map {|f| _compile_filter(f)} + filter.map {|f| _compile_source(f)} when Symbol filter when String "(#{filter})" - when Proc + when ::Proc method_name = "_callback_#{@kind}_#{next_id}" @klass.send(:define_method, method_name, &filter) return method_name if filter.arity <= 0 - method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") + method_name << (filter.arity == 1 ? "(self)" : "(self, ::Proc.new)") else method_name = _method_name_for_object_filter(kind, filter) - @_is_object_filter = true @klass.send(:define_method, "#{method_name}_object") { filter } _normalize_legacy_filter(kind, filter) scopes = Array(chain.config[:scope]) - method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") + method_to_call = scopes.map{ |s| public_send(s) }.join("_") @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{method_name}(&blk) @@ -465,7 +491,7 @@ module ActiveSupport __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| mapped ||= filters.map do |filter| - Callback.new(chain, filter, type, options.dup, self) + Callback.build(chain, filter, type, options.dup, self) end options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 8df1c5bb7a..9659f141cb 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -802,14 +802,85 @@ module CallbacksTest end end + class ConditionalTests < ActiveSupport::TestCase + def build_class(callback) + Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo + set_callback :foo, :before, :foo, :if => callback + def foo; end + def run; run_callbacks :foo; end + } + end + + # FIXME: do we really want to support classes as conditionals? There were + # no tests for it previous to this. + def test_class_conditional_with_scope + z = [] + callback = Class.new { + define_singleton_method(:foo) { |o| z << o } + } + klass = Class.new { + include ActiveSupport::Callbacks + define_callbacks :foo, :scope => [:name] + set_callback :foo, :before, :foo, :if => callback + def foo; end + def run; run_callbacks :foo; end + } + object = klass.new + object.run + assert_equal [object], z + end + + # FIXME: do we really want to support classes as conditionals? There were + # no tests for it previous to this. + def test_class + z = [] + klass = build_class Class.new { + define_singleton_method(:before) { |o| z << o } + } + object = klass.new + object.run + assert_equal [object], z + end + + def test_proc_negative_arity # passes an empty list if *args + z = [] + object = build_class(->(*args) { z << args }).new + object.run + assert_equal [], z.flatten + end + + def test_proc_arity0 + z = [] + object = build_class(->() { z << 0 }).new + object.run + assert_equal [0], z + end + + def test_proc_arity1 + z = [] + object = build_class(->(x) { z << x }).new + object.run + assert_equal [object], z + end + + def test_proc_arity2 + assert_raises(ArgumentError) do + object = build_class(->(a,b) { }).new + object.run + end + end + end + class CallbackTypeTest < ActiveSupport::TestCase def build_class(callback, n = 10) Class.new { include ActiveSupport::Callbacks define_callbacks :foo - n.times { set_callback :foo, callback } + n.times { set_callback :foo, :before, callback } def run; run_callbacks :foo; end - def self.skip(thing); skip_callback :foo, thing; end + def self.skip(thing); skip_callback :foo, :before, thing; end } end |