Skip to content

Commit

Permalink
Move validation check to thread local to avoid infinite loops.
Browse files Browse the repository at this point in the history
- Instance variables not safe since reload breaks bindings.
- Fixes #1151.
  • Loading branch information
durran committed Aug 21, 2011
1 parent ad5e156 commit 348b684
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 47 deletions.
3 changes: 2 additions & 1 deletion lib/mongoid/relations/accessors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def getter(name, metadata)
def setter(name, metadata)
tap do
define_method("#{name}=") do |object|
if relation_exists?(name) || metadata.many?
if relation_exists?(name) || metadata.many? ||
(object.blank? && send(name))
set_relation(name, send(name).substitute(object.substitutable))
else
build(name, object.substitutable, metadata)
Expand Down
4 changes: 2 additions & 2 deletions lib/mongoid/relations/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ class Binding
#
# @since 2.1.0
def binding
Threaded.binding = true
Threaded.begin_bind
yield
ensure
Threaded.binding = false
Threaded.exit_bind
end

# Is the current thread in binding mode?
Expand Down
117 changes: 102 additions & 15 deletions lib/mongoid/threaded.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ module Mongoid #:nodoc:
module Threaded
extend self

# Begins a binding block.
#
# @example Begin the bind.
# Threaded.begin_bind
#
# @return [ true ] Always true.
#
# @since 2.1.9
def begin_bind
bind_stack.push(true)
end

# Begins a building block.
#
# @example Begin the build.
Expand All @@ -18,30 +30,28 @@ def begin_build
build_stack.push(true)
end

# Is the current thread in binding mode?
# Begin validating a document on the current thread.
#
# @example Is the thread in binding mode?
# Threaded.binding?
# @example Begin validation.
# Threaded.begin_validate(doc)
#
# @return [ true, false ] If the thread is in binding mode?
# @param [ Document ] document The document to validate.
#
# @since 2.1.0
def binding?
Thread.current[:"[mongoid]:binding-mode"] ||= false
# @since 2.1.9
def begin_validate(document)
validations_for(document.class).push(document.id)
end

# Set the binding mode for the current thread.
#
# @example Set the binding mode.
# Threaded.binding = true
# Is the current thread in binding mode?
#
# @param [ true, false ] mode The current binding mode.
# @example Is the thread in binding mode?
# Threaded.binding?
#
# @return [ true, false ] The current binding mode.
# @return [ true, false ] If the thread is in binding mode?
#
# @since 2.1.0
def binding=(mode)
Thread.current[:"[mongoid]:binding-mode"] = mode
def binding?
!bind_stack.empty?
end

# Is the current thread in building mode?
Expand All @@ -56,6 +66,19 @@ def building?
!build_stack.empty?
end

# Get the bind stack for the current thread. Is simply an array of calls
# to Mongoid's binding method.
#
# @example Get the bind stack.
# Threaded.bind_stack
#
# @return [ Array ] The array of bind calls.
#
# @since 2.1.9
def bind_stack
Thread.current[:"[mongoid]:bind-stack"] ||= []
end

# Get the build stack for the current thread. Is simply an array of calls
# to Mongoid's building method.
#
Expand All @@ -81,6 +104,18 @@ def clear_safety_options!
Thread.current[:"[mongoid]:safety-options"] = nil
end

# Exit the binding block.
#
# @example Exit the binding block.
# Threaded.exit_bind
#
# @return [ true ] The last element in the stack.
#
# @since 2.1.9
def exit_bind
bind_stack.pop
end

# Exit the building block.
#
# @example Exit the building block.
Expand All @@ -93,6 +128,18 @@ def exit_build
build_stack.pop
end

# Exit validating a document on the current thread.
#
# @example Exit validation.
# Threaded.exit_validate(doc)
#
# @param [ Document ] document The document to validate.
#
# @since 2.1.9
def exit_validate(document)
validations_for(document.class).delete_one(document.id)
end

# Get the identity map off the current thread.
#
# @example Get the identity map.
Expand Down Expand Up @@ -194,5 +241,45 @@ def update_consumer(klass)
def set_update_consumer(klass, consumer)
Thread.current[:"[mongoid][#{klass}]:update-consumer"] = consumer
end

# Is the document validated on the current thread?
#
# @example Is the document validated?
# Threaded.validated?(doc)
#
# @param [ Document ] document The document to check.
#
# @return [ true, false ] If the document is validated.
#
# @since 2.1.9
def validated?(document)
validations_for(document.class).include?(document.id)
end

# Get all validations on the current thread.
#
# @example Get all validations.
# Threaded.validations
#
# @return [ Hash ] The current validations.
#
# @since 2.1.9
def validations
Thread.current[:"[mongoid]:validations"] ||= {}
end

# Get all validations on the current thread for the class.
#
# @example Get all validations.
# Threaded.validations_for(Person)
#
# @param [ Class ] The class to check.
#
# @return [ Array ] The current validations.
#
# @since 2.1.9
def validations_for(klass)
validations[klass] ||= []
end
end
end
22 changes: 20 additions & 2 deletions lib/mongoid/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,25 @@ module Validations
extend ActiveSupport::Concern
include ActiveModel::Validations

attr_accessor :validated
# Begin the associated validation.
#
# @example Begin validation.
# document.begin_validate
#
# @since 2.1.9
def begin_validate
Threaded.begin_validate(self)
end

# Exit the associated validation.
#
# @example Exit validation.
# document.exit_validate
#
# @since 2.1.9
def exit_validate
Threaded.exit_validate(self)
end

# Overrides the default ActiveModel behaviour since we need to handle
# validations of relations slightly different than just calling the
Expand Down Expand Up @@ -59,7 +77,7 @@ def valid?(context = nil)
#
# @since 2.0.0.rc.2
def validated?
!!@validated
Threaded.validated?(self)
end

module ClassMethods #:nodoc:
Expand Down
21 changes: 12 additions & 9 deletions lib/mongoid/validations/associated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,18 @@ class AssociatedValidator < ActiveModel::EachValidator
# @param [ Symbol ] attribute The relation to validate.
# @param [ Object ] value The value of the relation.
def validate_each(document, attribute, value)
document.validated = true
valid = Array.wrap(value).collect do |doc|
if doc.nil?
true
else
doc.validated? ? true : doc.valid?
end
end.all?
document.validated = false
begin
document.begin_validate
valid = Array.wrap(value).collect do |doc|
if doc.nil?
true
else
doc.validated? ? true : doc.valid?
end
end.all?
ensure
document.exit_validate
end
return if valid
document.errors.add(attribute, :invalid, options.merge(:value => value))
end
Expand Down
Loading

0 comments on commit 348b684

Please sign in to comment.