Skip to content
This repository has been archived by the owner on Dec 12, 2018. It is now read-only.

Commit

Permalink
adding recursion control to valid?(), save(), and changed?()
Browse files Browse the repository at this point in the history
  • Loading branch information
cainlevy committed Dec 14, 2008
1 parent 64d8249 commit cfba211
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 6 deletions.
20 changes: 14 additions & 6 deletions lib/nested_assignment.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NestedAssignment
module NestedAssignment
include RecursionControl

def self.included(base)
base.class_eval do
extend ClassMethods
Expand Down Expand Up @@ -60,15 +62,19 @@ def association_names
# validation process, so that all errors will be available
# afterwards.
def valid_with_associated?(*args)
[modified_associated.all?(&:valid?), valid_without_associated?(*args)].all?
without_recursion(:valid?) do
[modified_associated.all?(&:valid?), valid_without_associated?(*args)].all?
end
end

# deep saving of any new, changed, or deleted records.
def save_with_associated(*args)
self.class.transaction do
save_without_associated(*args) &&
modified_associated.all?{|a| a.save} &&
deletable_associated.all?{|a| a.destroy}
without_recursion(:save) do
self.class.transaction do
save_without_associated(*args) &&
modified_associated.all?{|a| a.save} &&
deletable_associated.all?{|a| a.destroy}
end
end
end

Expand All @@ -78,7 +84,9 @@ def save_with_associated(*args)
# the user, we would want to say that the task had changed so we
# could then recurse and discover that the tag had changed.
def changed_with_associated?
changed_without_associated? or changed_associated
without_recursion(:save) do
changed_without_associated? or changed_associated
end
end

protected
Expand Down
27 changes: 27 additions & 0 deletions lib/recursion_control.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module RecursionControl
class << self
def stack
@stack ||= {}
end
end

# Shortcuts recursion by assuming a default return value.
#
# For example: consider what would happen if two associated
# records each tried to validate the other. They would loop
# recursively calling #valid? on the other until Ruby grew
# tired and raised StackTooDeep. But in this situation, each
# record is in fact valid because the other does in fact
# exist. So by replacing recursive references with a default
# value of true, we can remove the recursion without changing
# the result.
def without_recursion(method, default = true, &block)
RecursionControl.stack[method] ||= []

return default if RecursionControl.stack[method].include? self
RecursionControl.stack[method] << self
result = yield
RecursionControl.stack[method].delete(self)
return result
end
end
18 changes: 18 additions & 0 deletions test/unit/nested_assignment_saving_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,22 @@ def test_saving_with_modified_existing_deeply_associated_records
end
assert_equal "difficult", @user.reload.tasks[0].tags[0].name
end

def test_saving_with_recursive_references
# This recursive situation is a little contrived. A more likely example would be
# a new associated record that refers back to the first. For example, suppose you
# store events, and after the user modifies his name you wish to store the fact.
# You may do something like `Event.create(:user => self, :change => 'name')`. This
# would create a recursive reference such as here.
@user = users(:bob)
@user.name = "william"
@user.tasks[0].name = "research"
@user.tasks[0].user = @user
assert_nothing_raised do
@user.save
end
@user.reload
assert_equal "william", @user.name
assert_equal "research", @user.tasks[0].name
end
end

0 comments on commit cfba211

Please sign in to comment.