aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAaron Patterson <aaron.patterson@gmail.com>2013-05-14 12:04:18 -0700
committerAaron Patterson <aaron.patterson@gmail.com>2013-05-14 12:04:18 -0700
commite8e7d8357c5f641842f5c4730396fae4d423723a (patch)
tree791e0baf8624418851e2c7fea52f87ad85bcaafd
parent8ce3c1e5dde9fb180813e4d89324db03da110b13 (diff)
parent78202055c971659689f6a96a5b4aa2c138cb44d2 (diff)
downloadrails-e8e7d8357c5f641842f5c4730396fae4d423723a.tar.gz
rails-e8e7d8357c5f641842f5c4730396fae4d423723a.tar.bz2
rails-e8e7d8357c5f641842f5c4730396fae4d423723a.zip
Merge branch 'polymorphic'
* polymorphic: (41 commits) fix shadowed variable warnings polymorphic around callbacks polymorphic after filter rename terminal to halting, try to keep naming consistent push the before filter lambdas to factory methods polymorphic before callbacks use a singleton end node Revert "we never pass blocks, so remove this" if there is nothing to compile, then do not bother compiling Arrays are no longer supported we never pass blocks, so remove this raise an argument error if the filter arity is greater than 1 pass the actual filter, not a string do not keep a reference to the chain in the callback objects fix deprecation test push merge code to the callback itself dup the callback and set the chain remove klass because it is not used rename instance variables push duplicates? logic to the instance ...
-rw-r--r--activemodel/test/cases/validations_test.rb2
-rw-r--r--activerecord/test/cases/callbacks_test.rb2
-rw-r--r--activesupport/lib/active_support/callbacks.rb588
-rw-r--r--activesupport/test/callbacks_test.rb2
4 files changed, 364 insertions, 230 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 fc63fb8c8e..ab3bb23e88 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -76,8 +76,14 @@ module ActiveSupport
# save
# end
def run_callbacks(kind, &block)
- runner_name = self.class.__define_callbacks(kind, self)
- send(runner_name, &block)
+ cbs = send("_#{kind}_callbacks")
+ if cbs.empty?
+ yield if block_given?
+ else
+ runner = cbs.compile
+ e = Filters::Environment.new(self, false, nil, block)
+ runner.call(e).value
+ end
end
private
@@ -88,44 +94,259 @@ module ActiveSupport
def halted_callback_hook(filter)
end
- class Callback #:nodoc:#
- @@_callback_sequence = 0
+ module Filters
+ Environment = Struct.new(:target, :halted, :value, :run_block)
- class Basic < Callback
+ class End
+ def call(env)
+ block = env.run_block
+ env.value = !env.halted && (!block || block.call)
+ env
+ end
end
+ ENDING = End.new
+
+ class Before
+ def self.build(next_callback, user_callback, user_conditions, chain_config, filter)
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halted_lambda = eval "lambda { |result| #{chain_config[:terminator]} }"
+ halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ elsif chain_config.key? :terminator
+ halted_lambda = eval "lambda { |result| #{chain_config[:terminator]} }"
+ halting(next_callback, user_callback, halted_lambda, filter)
+ elsif user_conditions.any?
+ conditional(next_callback, user_callback, user_conditions)
+ else
+ simple next_callback, user_callback
+ end
+ end
- class Object < Callback
- def duplicates?(other)
- false
+ private
+
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ 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
+ end
+ end
+ next_callback.call env
+ }
+ end
+
+ def self.halting(next_callback, user_callback, halted_lambda, filter)
+ lambda { |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ if !halted
+ result = user_callback.call target, value
+ env.halted = halted_lambda.call result
+ if env.halted
+ target.send :halted_callback_hook, filter
+ end
+ end
+ next_callback.call env
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+ next_callback.call env
+ }
+ end
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ user_callback.call env.target, env.value
+ next_callback.call env
+ }
+ end
+ end
+
+ class After
+ def self.build(next_callback, user_callback, user_conditions, chain_config)
+ if chain_config[:skip_after_callbacks_if_terminated]
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halting_and_conditional(next_callback, user_callback, user_conditions)
+ elsif chain_config.key?(:terminator)
+ halting(next_callback, user_callback)
+ elsif user_conditions.any?
+ conditional next_callback, user_callback, user_conditions
+ else
+ simple next_callback, user_callback
+ end
+ else
+ if user_conditions.any?
+ conditional next_callback, user_callback, user_conditions
+ else
+ simple next_callback, user_callback
+ end
+ end
+ end
+
+ private
+
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions)
+ 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
+ }
+ end
+
+ def self.halting(next_callback, user_callback)
+ lambda { |env|
+ env = next_callback.call env
+ if !env.halted
+ user_callback.call env.target, env.value
+ end
+ env
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ env = next_callback.call env
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+ env
+ }
+ end
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ env = next_callback.call env
+ user_callback.call env.target, env.value
+ env
+ }
+ end
+ end
+
+ class Around
+ def self.build(next_callback, user_callback, user_conditions, chain_config)
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halting_and_conditional(next_callback, user_callback, user_conditions)
+ elsif chain_config.key? :terminator
+ halting(next_callback, user_callback)
+ elsif user_conditions.any?
+ conditional(next_callback, user_callback, user_conditions)
+ else
+ simple(next_callback, user_callback)
+ end
+ end
+
+ private
+
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions)
+ 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
+
+ def self.halting(next_callback, user_callback)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if !env.halted
+ user_callback.call(target, value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ else
+ next_callback.call env
+ end
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if 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
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ user_callback.call(env.target, env.value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ }
end
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
+ class Callback #:nodoc:#
+ def self.build(chain, filter, kind, options)
+ new chain.name, filter, kind, options, chain.config
end
- attr_accessor :chain, :kind, :options, :klass, :raw_filter
+ attr_accessor :kind, :options, :name
+ attr_reader :chain_config
+
+ 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 +354,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,144 +373,42 @@ 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})
- end
- end
- #{code}
- RUBY_EVAL
+ Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter)
when :after
- <<-RUBY_EVAL
- #{code}
- if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options}
- #{@source}
- end
- RUBY_EVAL
+ Filters::After.build(next_callback, user_callback, user_conditions, chain_config)
when :around
- name = define_conditional_callback
- <<-RUBY_EVAL
- #{name}(halted) do
- #{code}
- value
- end
- RUBY_EVAL
+ Filters::Around.build(next_callback, user_callback, user_conditions, chain_config)
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:
#
- # 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.
@@ -294,43 +417,59 @@ module ActiveSupport
# 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_source(filter)
+ def make_lambda(filter)
case filter
- when Array
- filter.map {|f| _compile_source(f)}
when Symbol
- filter
+ lambda { |target, _, &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
+ raise ArgumentError if filter.arity > 1
- method_name << (filter.arity == 1 ? "(self)" : "(self, ::Proc.new)")
+ if filter.arity <= 0
+ lambda { |target, _| target.instance_exec(&filter) }
+ else
+ lambda { |target, _| target.instance_exec(target, &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
+
+ 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
- method_name
+ 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)
@@ -354,27 +493,53 @@ 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)
@name = name
@config = {
- :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)
- end
- method << callbacks
+ return @callbacks if @callbacks
- method << "value"
- method.join("\n")
+ @callbacks = Filters::ENDING
+ @chain.reverse_each do |callback|
+ @callbacks = callback.apply(@callbacks)
+ end
+ @callbacks
end
def append(*callbacks)
@@ -385,58 +550,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
-
def normalize_callback_params(name, filters, block) # :nodoc:
type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
options = filters.last.is_a?(Hash) ? filters.pop : {}
@@ -450,7 +589,6 @@ module ActiveSupport
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
chain = target.get_callbacks name
yield target, chain.dup
- target.__reset_runner(name)
end
end
@@ -491,9 +629,9 @@ module ActiveSupport
# existing chain rather than appended.
def set_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(name, filter_list, block)
- chain = get_callbacks name
+ self_chain = get_callbacks name
mapped = filters.map do |filter|
- Callback.build(chain, filter, type, options.dup, self)
+ Callback.build(self_chain, filter, type, options.dup)
end
__update_callbacks(name) do |target, chain|
@@ -517,9 +655,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)
@@ -536,12 +673,9 @@ module ActiveSupport
chain = target.get_callbacks(symbol).dup
callbacks.each { |c| chain.delete(c) }
target.set_callbacks symbol, chain
- target.__reset_runner(symbol)
end
self.set_callbacks symbol, callbacks.dup.clear
-
- __reset_runner(symbol)
end
# Define sets of events in the object lifecycle that support callbacks.
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index af2de7845f..86057f7cbb 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -718,7 +718,7 @@ module CallbacksTest
def test_termination_invokes_hook
terminator = CallbackTerminator.new
terminator.save
- assert_equal ":second", terminator.halted
+ assert_equal :second, terminator.halted
end
def test_block_never_called_if_terminated