Skip to content

Commit

Permalink
Extract ActiveSupport::Callbacks from Active Record, test case setup …
Browse files Browse the repository at this point in the history
…and teardown, and ActionController::Dispatcher. Closes #10727.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@8664 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
jeremy committed Jan 19, 2008
1 parent 3ffdfa8 commit aae37bb
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 216 deletions.
51 changes: 13 additions & 38 deletions actionpack/lib/action_controller/dispatcher.rb
Expand Up @@ -11,20 +11,6 @@ def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, o
new(output).dispatch_cgi(cgi, session_options)
end

# Declare a block to be called before each dispatch.
# Run in the order declared.
def before_dispatch(*method_names, &block)
callbacks[:before].concat method_names
callbacks[:before] << block if block_given?
end

# Declare a block to be called after each dispatch.
# Run in reverse of the order declared.
def after_dispatch(*method_names, &block)
callbacks[:after].concat method_names
callbacks[:after] << block if block_given?
end

# Add a preparation callback. Preparation callbacks are run before every
# request in development mode, and before the first request in production
# mode.
Expand All @@ -34,15 +20,16 @@ def after_dispatch(*method_names, &block)
# existing callback. Passing an identifier is a suggested practice if the
# code adding a preparation block may be reloaded.
def to_prepare(identifier = nil, &block)
@prepare_dispatch_callbacks ||= []
callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier)

# Already registered: update the existing callback
if identifier
if callback = callbacks[:prepare].assoc(identifier)
callback[1] = block
else
callbacks[:prepare] << [identifier, block]
end
# TODO: Ruby one liner for Array#find returning index
if identifier && callback_for_identifier = @prepare_dispatch_callbacks.find { |c| c.identifier == identifier }
index = @prepare_dispatch_callbacks.index(callback_for_identifier)
@prepare_dispatch_callbacks[index] = callback
else
callbacks[:prepare] << block
@prepare_dispatch_callbacks.concat([callback])
end
end

Expand Down Expand Up @@ -90,12 +77,11 @@ def failsafe_logger
cattr_accessor :error_file_path
self.error_file_path = "#{::RAILS_ROOT}/public" if defined? ::RAILS_ROOT

cattr_accessor :callbacks
self.callbacks = Hash.new { |h, k| h[k] = [] }

cattr_accessor :unprepared
self.unprepared = true

include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch

before_dispatch :reload_application
before_dispatch :prepare_application
Expand All @@ -115,12 +101,12 @@ def initialize(output, request = nil, response = nil)
def dispatch
@@guard.synchronize do
begin
run_callbacks :before
run_callbacks :before_dispatch
handle_request
rescue Exception => exception
failsafe_rescue exception
ensure
run_callbacks :after, :reverse_each
run_callbacks :after_dispatch, :enumerator => :reverse_each
end
end
end
Expand Down Expand Up @@ -152,7 +138,7 @@ def prepare_application(force = false)
ActiveRecord::Base.verify_active_connections! if defined?(ActiveRecord)

if unprepared || force
run_callbacks :prepare
run_callbacks :prepare_dispatch
self.unprepared = false
end
end
Expand All @@ -177,17 +163,6 @@ def handle_request
@controller.process(@request, @response).out(@output)
end

def run_callbacks(kind, enumerator = :each)
callbacks[kind].send!(enumerator) do |callback|
case callback
when Proc; callback.call(self)
when String, Symbol; send!(callback)
when Array; callback[1].call(self)
else raise ArgumentError, "Unrecognized callback #{callback.inspect}"
end
end
end

def failsafe_rescue(exception)
self.class.failsafe_response(@output, '500 Internal Server Error', exception) do
if @controller ||= defined?(::ApplicationController) ? ::ApplicationController : Base
Expand Down
2 changes: 1 addition & 1 deletion actionpack/test/controller/dispatcher_test.rb
Expand Up @@ -11,7 +11,7 @@ def setup
@output = StringIO.new
ENV['REQUEST_METHOD'] = 'GET'

Dispatcher.callbacks[:prepare].clear
Dispatcher.instance_variable_set("@prepare_dispatch_callbacks", [])
@dispatcher = Dispatcher.new(@output)
end

Expand Down
41 changes: 6 additions & 35 deletions activerecord/lib/active_record/callbacks.rb
Expand Up @@ -183,14 +183,8 @@ def self.included(base) #:nodoc:
base.send :alias_method_chain, method, :callbacks
end

CALLBACKS.each do |method|
base.class_eval <<-"end_eval"
def self.#{method}(*callbacks, &block)
callbacks << block if block_given?
write_inheritable_array(#{method.to_sym.inspect}, callbacks)
end
end_eval
end
base.send :include, ActiveSupport::Callbacks
base.define_callbacks *CALLBACKS
end

# Is called when the object was instantiated by one of the finders, like <tt>Base.find</tt>.
Expand Down Expand Up @@ -301,38 +295,15 @@ def destroy_with_callbacks #:nodoc:
def callback(method)
notify(method)

callbacks_for(method).each do |callback|
result = case callback
when Symbol
self.send(callback)
when String
eval(callback, binding)
when Proc, Method
callback.call(self)
else
if callback.respond_to?(method)
callback.send(method, self)
else
raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
end
end
return false if result == false
end
result = run_callbacks(method) { |result, object| result == false }

result = send(method) if respond_to_without_attributes?(method)
if result != false && respond_to_without_attributes?(method)
result = send(method)
end

return result
end

def callbacks_for(method)
self.class.read_inheritable_attribute(method.to_sym) or []
end

def invoke_and_notify(method)
notify(method)
send(method) if respond_to_without_attributes?(method)
end

def notify(method) #:nodoc:
self.class.changed
self.class.notify_observers(method, self)
Expand Down
122 changes: 31 additions & 91 deletions activerecord/lib/active_record/validations.rb
Expand Up @@ -279,6 +279,25 @@ def self.included(base) # :nodoc:
alias_method_chain :save!, :validation
alias_method_chain :update_attribute, :validation_skipping
end

base.send :include, ActiveSupport::Callbacks

# TODO: Use helper ActiveSupport::Callbacks#define_callbacks instead
%w( validate validate_on_create validate_on_update ).each do |validation_method|
base.class_eval <<-"end_eval"
def self.#{validation_method}(*methods, &block)
options = methods.extract_options!
methods << block if block_given?
methods.map! { |method| Callback.new(:#{validation_method}, method, options) }
existing_methods = read_inheritable_attribute(:#{validation_method}) || []
write_inheritable_attribute(:#{validation_method}, existing_methods | methods)
end
def self.#{validation_method}_callback_chain
read_inheritable_attribute(:#{validation_method}) || []
end
end_eval
end
end

# All of the following validations are defined in the class scope of the model that you're interested in validating.
Expand Down Expand Up @@ -324,43 +343,6 @@ module ClassMethods
# end
#
# This usage applies to #validate_on_create and #validate_on_update as well.
def validate(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate, methods)
end

def validate_on_create(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate_on_create, methods)
end

def validate_on_update(*methods, &block)
methods << block if block_given?
write_inheritable_set(:validate_on_update, methods)
end

def condition_block?(condition)
condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1)
end

# Determine from the given condition (whether a block, procedure, method or string)
# whether or not to validate the record. See #validates_each.
def evaluate_condition(condition, record)
case condition
when Symbol; record.send(condition)
when String; eval(condition, record.instance_eval { binding })
else
if condition_block?(condition)
condition.call(record)
else
raise(
ActiveRecordError,
"Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
"class implementing a static validation method"
)
end
end
end

# Validates each attribute against a block.
#
Expand All @@ -379,20 +361,17 @@ def evaluate_condition(condition, record)
# method, proc or string should return or evaluate to a true or false value.
# * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should
# not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The
# method, proc or string should return or evaluate to a true or false value.
# method, proc or string should return or evaluate to a true or false value.
def validates_each(*attrs)
options = attrs.extract_options!.symbolize_keys
attrs = attrs.flatten

# Declare the validation.
send(validation_method(options[:on] || :save)) do |record|
# Don't validate when there is an :if condition and that condition is false or there is an :unless condition and that condition is true
unless (options[:if] && !evaluate_condition(options[:if], record)) || (options[:unless] && evaluate_condition(options[:unless], record))
attrs.each do |attr|
value = record.send(attr)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
yield record, attr, value
end
send(validation_method(options[:on] || :save), options) do |record|
attrs.each do |attr|
value = record.send(attr)
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
yield record, attr, value
end
end
end
Expand Down Expand Up @@ -515,11 +494,9 @@ def validates_presence_of(*attr_names)

# can't use validates_each here, because it cannot cope with nonexistent attributes,
# while errors.add_on_empty can
send(validation_method(configuration[:on])) do |record|
unless (configuration[:if] && !evaluate_condition(configuration[:if], record)) || (configuration[:unless] && evaluate_condition(configuration[:unless], record))
record.errors.add_on_blank(attr_names, configuration[:message])
end
end
send(validation_method(configuration[:on]), configuration) do |record|
record.errors.add_on_blank(attr_names, configuration[:message])
end
end

# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
Expand Down Expand Up @@ -911,13 +888,7 @@ def create!(attributes = nil)
end
end


private
def write_inheritable_set(key, methods)
existing_methods = read_inheritable_attribute(key) || []
write_inheritable_attribute(key, existing_methods | methods)
end

def validation_method(on)
case on
when :save then :validate
Expand Down Expand Up @@ -959,14 +930,14 @@ def update_attribute_with_validation_skipping(name, value)
def valid?
errors.clear

run_validations(:validate)
run_callbacks(:validate)
validate

if new_record?
run_validations(:validate_on_create)
run_callbacks(:validate_on_create)
validate_on_create
else
run_validations(:validate_on_update)
run_callbacks(:validate_on_update)
validate_on_update
end

Expand All @@ -990,36 +961,5 @@ def validate_on_create #:doc:
# Overwrite this method for validation checks used only on updates.
def validate_on_update # :doc:
end

private
def run_validations(validation_method)
validations = self.class.read_inheritable_attribute(validation_method.to_sym)
if validations.nil? then return end
validations.each do |validation|
if validation.is_a?(Symbol)
self.send(validation)
elsif validation.is_a?(String)
eval(validation, binding)
elsif validation_block?(validation)
validation.call(self)
elsif validation_class?(validation, validation_method)
validation.send(validation_method, self)
else
raise(
ActiveRecordError,
"Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
"class implementing a static validation method"
)
end
end
end

def validation_block?(validation)
validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1)
end

def validation_class?(validation, validation_method)
validation.respond_to?(validation_method)
end
end
end
2 changes: 1 addition & 1 deletion activerecord/test/cases/validations_test.rb
Expand Up @@ -1017,7 +1017,7 @@ def test_validate_block

def test_invalid_validator
Topic.validate 3
assert_raise(ActiveRecord::ActiveRecordError) { t = Topic.create }
assert_raise(ArgumentError) { t = Topic.create }
end

def test_throw_away_typing
Expand Down
2 changes: 2 additions & 0 deletions activesupport/CHANGELOG
@@ -1,5 +1,7 @@
*SVN*

* Extract ActiveSupport::Callbacks from Active Record, test case setup and teardown, and ActionController::Dispatcher. #10727 [Josh Peek]

* Introducing DateTime #utc, #utc? and #utc_offset, for duck-typing compatibility with Time. Closes #10002 [Geoff Buesing]

* Time#to_json uses Numeric#to_utc_offset_s to output a cross-platform-consistent representation without having to convert to DateTime. References #9750 [Geoff Buesing]
Expand Down
1 change: 1 addition & 0 deletions activesupport/lib/active_support.rb
Expand Up @@ -26,6 +26,7 @@
require 'active_support/vendor'
require 'active_support/basic_object'
require 'active_support/inflector'
require 'active_support/callbacks'

require 'active_support/core_ext'

Expand Down

0 comments on commit aae37bb

Please sign in to comment.