module ActiveFedora::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 ActiveFedora::Base#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)

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, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
  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)
                              Array(attributes_collection)
                            else
                              attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
                            end
  end
  association = send(association_name)
  existing_records = if association.loaded?
                       association.target
                     else
                       attribute_ids = attributes_collection.map { |a| a['id'] || a[:id] }.compact
                       attribute_ids.present? ? association.to_a.select { |x| attribute_ids.include?(x.id) } : []
                     end
  attributes_collection.each do |attributes|
    attributes = attributes.to_h if attributes.respond_to?(:permitted)
    attributes = attributes.with_indifferent_access
    if attributes['id'].blank?
      unless reject_new_record?(association_name, attributes)
        association.build(attributes.except(*UNASSIGNABLE_KEYS))
      end
    elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
      association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes)
      unless call_reject_if(association_name, attributes)
        assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
      end
    else
      raise_nested_attributes_record_not_found!(association_name, attributes['id'])
    end
  end
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)
  options = nested_attributes_options[association_name]
  attributes = attributes.to_h if attributes.respond_to?(:permitted?)
  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.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)
  opts = nested_attributes_options[association_name]
  case callback = opts[: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)
  return unless limit
  limit = case limit
          when Symbol
            send(limit)
          when Proc
            limit.call
          else
            limit
          end
  raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." if limit && attributes_collection.size > limit
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)
  reflection = self.class._reflect_on_association(association_name).klass.name
  raise ObjectNotFoundError, "Couldn't find #{reflection.klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{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