module ActiveRecord::NestedAttributes

def _destroy

See ActionView::Helpers::FormHelper#fields_for for more info.

destruction of this association.
used in conjunction with fields_for to build a form element for the
Returns ActiveRecord::AutosaveAssociation#marked_for_destruction? It's
def _destroy
  marked_for_destruction?
end

def allow_destroy?(association_name)

def allow_destroy?(association_name)
  nested_attributes_options[association_name][:allow_destroy]
end

def assign_nested_attributes_for_collection_association(association_name, attributes_collection)

])
{ id: '2', _destroy: true }
{ name: 'John' },
{ id: '1', name: 'Peter' },
assign_nested_attributes_for_collection_association(:people, [

Also accepts an Array of attribute hashes:

for destruction.
person with the name 'John', and mark the associated Person with ID 2
Will update the name of the Person with ID 1, build a new associated

})
'3' => { id: '2', _destroy: true }
'2' => { name: 'John' },
'1' => { id: '1', name: 'Peter' },
assign_nested_attributes_for_collection_association(:people, {

For example:

matched record for destruction.
value and a :_destroy key set to a truthy value will mark the
a new record for the association. Hashes with a matching :id
will update that record. Hashes without an :id value will build
Hashes with an :id value matching an existing associated record

Assigns the given attributes to the collection association.
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
  options = nested_attributes_options[association_name]
  if attributes_collection.respond_to?(:permitted?)
    attributes_collection = attributes_collection.to_h
  end
  unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
    raise ArgumentError, "Hash or Array expected for `#{association_name}` attributes, got #{attributes_collection.class.name}"
  end
  check_record_limit!(options[:limit], attributes_collection)
  if attributes_collection.is_a? Hash
    keys = attributes_collection.keys
    attributes_collection = if keys.include?("id") || keys.include?(:id)
      [attributes_collection]
    else
      attributes_collection.values
    end
  end
  association = association(association_name)
  existing_records = if association.loaded?
    association.target
  else
    attribute_ids = attributes_collection.filter_map { |a| a["id"] || a[:id] }
    attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids)
  end
  records = attributes_collection.map do |attributes|
    if attributes.respond_to?(:permitted?)
      attributes = attributes.to_h
    end
    attributes = attributes.with_indifferent_access
    if attributes["id"].blank?
      unless reject_new_record?(association_name, attributes)
        association.reader.build(attributes.except(*UNASSIGNABLE_KEYS))
      end
    elsif existing_record = find_record_by_id(association.klass, existing_records, attributes["id"])
      unless call_reject_if(association_name, attributes)
        # Make sure we are operating on the actual object which is in the association's
        # proxy_target array (either by finding it, or adding it if not found)
        # Take into account that the proxy_target may have changed due to callbacks
        target_record = find_record_by_id(association.klass, association.target, attributes["id"])
        if target_record
          existing_record = target_record
        else
          association.add_to_target(existing_record, skip_callbacks: true)
        end
        assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
        existing_record
      end
    else
      raise_nested_attributes_record_not_found!(association_name, attributes["id"])
    end
  end
  association.nested_attributes_target = records
end

def assign_nested_attributes_for_one_to_one_association(association_name, attributes)

then the existing record will be marked for destruction.
update_only is true, and a :_destroy key set to a truthy value,
If the given attributes include a matching :id attribute, or

record will be modified regardless of whether an :id is provided.
it will be replaced with a new record. If update_only is +true+ the existing
id, then the existing record will be modified. If no :id is provided
given attributes include an :id that matches the existing record's
the value of the update_only option. If update_only is +false+ and the
an associated record already exists, the method's behavior depends on
If an associated record does not yet exist, one will be instantiated. If

Assigns the given attributes to the association.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
  if attributes.respond_to?(:permitted?)
    attributes = attributes.to_h
  end
  unless attributes.is_a?(Hash)
    raise ArgumentError, "Hash expected for `#{association_name}` attributes, got #{attributes.class.name}"
  end
  options = nested_attributes_options[association_name]
  attributes = attributes.with_indifferent_access
  existing_record = send(association_name)
  if (options[:update_only] || !attributes["id"].blank?) && existing_record &&
      (options[:update_only] || existing_record.id.to_s == attributes["id"].to_s)
    assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
  elsif attributes["id"].present?
    raise_nested_attributes_record_not_found!(association_name, attributes["id"])
  elsif !reject_new_record?(association_name, attributes)
    assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
    if existing_record && existing_record.new_record?
      existing_record.assign_attributes(assignable_attributes)
      association(association_name).initialize_attributes(existing_record)
    else
      method = :"build_#{association_name}"
      if respond_to?(method)
        send(method, assignable_attributes)
      else
        raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
      end
    end
  end
end

def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)

+allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
Updates a record with the +attributes+ or marks it for destruction if
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
  record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
  record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy
end

def call_reject_if(association_name, attributes)

Returns false if there is a +destroy_flag+ on the attributes.

The reject_if option is defined by +accepts_nested_attributes_for+.
rejected by calling the reject_if Symbol or Proc (if defined).
Determines if a record with the particular +attributes+ should be
def call_reject_if(association_name, attributes)
  return false if will_be_destroyed?(association_name, attributes)
  case callback = nested_attributes_options[association_name][:reject_if]
  when Symbol
    method(callback).arity == 0 ? send(callback) : send(callback, attributes)
  when Proc
    callback.call(attributes)
  end
end

def check_record_limit!(limit, attributes_collection)

larger than the limit.
Raises TooManyRecords error if the attributes_collection is

number-like object (anything that can be compared with an integer).
records. It accepts limit in the form of symbol, proc, or
Takes in a limit and checks if the attributes_collection has too many
def check_record_limit!(limit, attributes_collection)
  if limit
    limit = \
      case limit
      when Symbol
        send(limit)
      when Proc
        limit.call
      else
        limit
      end
    if limit && attributes_collection.size > limit
      raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
    end
  end
end

def find_record_by_id(klass, records, id)

def find_record_by_id(klass, records, id)
  if klass.composite_primary_key?
    id = Array(id).map(&:to_s)
    records.find { |record| Array(record.id).map(&:to_s) == id }
  else
    records.find { |record| record.id.to_s == id.to_s }
  end
end

def has_destroy_flag?(hash)

Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
  Type::Boolean.new.cast(hash["_destroy"])
end

def raise_nested_attributes_record_not_found!(association_name, record_id)

def raise_nested_attributes_record_not_found!(association_name, record_id)
  model = self.class._reflect_on_association(association_name).klass.name
  raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
                           model, "id", record_id)
end

def reject_new_record?(association_name, attributes)

association and evaluates to +true+.
has_destroy_flag? or if a :reject_if proc exists for this
Determines if a new record should be rejected by checking
def reject_new_record?(association_name, attributes)
  will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes)
end

def will_be_destroyed?(association_name, attributes)

Only take into account the destroy flag if :allow_destroy is true
def will_be_destroyed?(association_name, attributes)
  allow_destroy?(association_name) && has_destroy_flag?(attributes)
end