Skip to content

Instantly share code, notes, and snippets.

@Nerdman4U
Created February 17, 2014 15:35
Show Gist options
  • Save Nerdman4U/9052758 to your computer and use it in GitHub Desktop.
Save Nerdman4U/9052758 to your computer and use it in GitHub Desktop.
Autosave, inverse and nested record stack level too deep problem fix
module ActiveRecord
module AutosaveAssociation
# Structure is a nested hash containing symbol keys as a valid
# reflection names, use only hash values. If you use nil value and
# nested record class has own structure, that will be used instead.
#
# ==== Example
#
# class Foo
# def self.structure
# { coffee: { honeys: { rock-n-roll: {} }, bees: {} } }
# end
# end
def structure
@structure
end
# Set current structure for this model. This is set by parent
# record.
def structure= value
@structure = value
end
# Verifies does the currect structure contain this reflection
#
# ==== Returns
#
# ::true
# if reflection.name is found from structure or if structure is
# omitted.
#
# ::false
# if structure is found but reflection.name is not found from it.
#
def reflection_in_structure? reflection
return true unless structure
structure.include?(reflection.name.to_sym)
end
# Validation of nested records is skipped if either of following is true,
#
# a) Skip option used at parent (this) record
#
# >> foo.save(validate: false)
# => skip validation, skip nested validation,
# do not skip other callbacks.
#
# b) Structure is in use but does not contain reflection name
#
# Example:
# >> foo.valid? structure: :cool_stuff
# => only reflection.targets of "cool stuff" will be validated.
#
def skip_association_validation? reflection
return true if skip_validations?
return true unless reflection_in_structure?(reflection)
false
end
# Are we done?
def validated_for_autosave?
@validated_for_autosave
end
# Mark this record valid so that following possible nested inverse
# association validation will not try to validate this ( and all
# reflections ) again.
def validated_for_autosave!
@validated_for_autosave = true
end
private
# Nested record validation will cause stack level too deep error if
# association has records which are inverse associtions to current
# record. Prevent this by setting a flag "validated_for_autosave"
# which is used to skip a record from inverse
# nested_records_changed_for_autosave? call.
#
# ==== Example
#
# class Foo
# has_many :bars, inverse_of: :foo
# accepts_nested_attributes_for :bars
#
# class Bar
# belongs_to :foo, inverse_of: bars
# accepts_nested_attributes_for :foo
def nested_records_changed_for_autosave?
validated_for_autosave!
self.class.reflect_on_all_autosave_associations.any? do |reflection|
association = association_instance_get(reflection.name)
next unless association
Array.wrap(association.target).any? do |object|
next if object.validated_for_autosave?
object.changed_for_autosave?
end
end
end
# Just simplified code here, nothing new
def records_for_validate_collection reflection
association = association_instance_get(reflection.name)
return [] unless association.present?
associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
end
def validate_collection_association(reflection)
return if skip_association_validation? reflection
records_for_validate_collection(reflection).each do |record|
association_valid?(reflection, record)
end
end
alias :original_validate_single_association :validate_single_association
def validate_single_association reflection
return if skip_association_validation? reflection
original_validate_single_association(reflection)
end
alias :original_association_valid? :association_valid?
def association_valid?(reflection, record)
if structure and reflection_in_structure?(reflection) and !record.destroyed?
record.structure = structure[reflection.name.to_sym]
end
original_association_valid?(reflection, record)
end
alias :original_save_collection_association :save_collection_association
def save_collection_association(reflection)
return unless reflection_in_structure?(reflection)
original_save_collection_association(reflection)
end
alias :original_save_has_one_association :save_has_one_association
def save_has_one_association(reflection)
return unless reflection_in_structure?(reflection)
original_save_has_one_association(reflection)
end
alias :original_save_belongs_to_association :save_belongs_to_association
def save_belongs_to_association(reflection)
return unless reflection_in_structure?(reflection)
original_save_belongs_to_association(reflection)
end
end
end
module ActiveRecord
# Use structures to prevent stack level too deep problems with
# autosave and nested records.
#
# ==== Usage
#
# Type class method "structure" to your class, let it return a hash
# of symbols. Similar as includes, joins etc.
#
# Objects of that class are now validated and saved based on structure
# depth.
#
# You can use structures explicitely by
# >> foo.save( structure: { bar:{doh:{}}} )
#
# Use only hash values
module Validations
# Save overwritten due to following problems,
#
# 1) { validate: false } option will skip all callbacks
#
# 2) { validate: false } option did not affect to nested autosave
# records, they are validated even option is set
#
# 3) structure is set here, if any
#
def save(options={})
skip_validations! if options[:validate] == false
set_structure! options
super if perform_validations(options)
end
def save!(options={})
skip_validations! if options[:validate] == false
set_structure! options
perform_validations(options) ? super : raise( RecordInvalid.new(self) )
end
def valid?( options=nil )
context = options unless options.is_a? Hash
context ||= (new_record? ? :create : :update)
options ||= {}
set_structure! options
output = super(context)
errors.empty? && output
end
private
# Set current structure to this record. This is given recursively
# from parent record.
#
# If options[:structure] exists, use it.
# If omitted but class has the default structure use class.structure.
# If omitted do not use structure.
def set_structure! options
@structure = options[:structure]
@structure = self.class.structure if !@structure and self.class.respond_to? :structure
end
def skip_validations?
!!@skip_validations
end
def skip_validations!
@skip_validations ||= true
end
protected
# Treat record as a valid if validations are skipped.
def perform_validations(options={})
return true if skip_validations?
valid?(options[:context])
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment