aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activemodel/lib/active_model/callbacks.rb30
-rw-r--r--activesupport/lib/active_support/callbacks.rb72
-rw-r--r--activesupport/test/callbacks_test.rb75
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