lib/mongoid/association/nested/one.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
module Association
module Nested
# Builder class used to perform #accepts_nested_attributes_for
# attribute assignment on one-to-n associations.
class One
include Buildable
attr_accessor :destroy
# Builds the association depending on the attributes and the options
# passed to the macro.
#
# @example Build a 1-1 nested document.
# one.build(person, as: :admin)
#
# @note This attempts to perform 3 operations, either one of an update of
# the existing association, a replacement of the association with a new
# document, or a removal of the association.
#
# @param [ Document ] parent The parent document.
#
# @return [ Document ] The built document.
def build(parent)
return if reject?(parent, attributes)
@existing = parent.send(association.name)
if update?
delete_id(attributes)
existing.assign_attributes(attributes)
elsif replace?
parent.send(association.setter, Factory.build(@class_name, attributes))
elsif delete?
parent.send(association.setter, nil)
else
check_for_id_violation!
end
end
# Create the new builder for nested attributes on one-to-one
# associations.
#
# @example Instantiate the builder.
# One.new(association, attributes)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
# @param [ Hash ] attributes The attributes hash to attempt to set.
# @param [ Hash ] options The options defined.
def initialize(association, attributes, options)
@attributes = attributes.with_indifferent_access
@association = association
@options = options
@class_name = options[:class_name] ? options[:class_name].constantize : association.klass
@destroy = @attributes.delete(:_destroy)
end
private
# Extracts and converts the id to the expected type.
#
# @return [ BSON::ObjectId | String | Object | nil ] The converted id,
# or nil if no id is present in the attributes hash.
def extracted_id
@extracted_id ||= begin
id = association.klass.extract_id_field(attributes)
convert_id(existing.class, id)
end
end
# Is the id in the attributes acceptable for allowing an update to
# the existing association?
#
# @api private
#
# @example Is the id acceptable?
# one.acceptable_id?
#
# @return [ true | false ] If the id part of the logic will allow an update.
def acceptable_id?
id = extracted_id
existing._id == id || id.nil? || (existing._id != id && update_only?)
end
# Can the existing association be deleted?
#
# @example Can the existing object be deleted?
# one.delete?
#
# @return [ true | false ] If the association should be deleted.
def delete?
id = association.klass.extract_id_field(attributes)
destroyable? && !id.nil?
end
# Can the existing association potentially be destroyed?
#
# @example Is the object destroyable?
# one.destroyable?({ :_destroy => "1" })
#
# @return [ true | false ] If the association can potentially be
# destroyed.
def destroyable?
Nested::DESTROY_FLAGS.include?(destroy) && allow_destroy?
end
# Is the document to be replaced?
#
# @example Is the document to be replaced?
# one.replace?
#
# @return [ true | false ] If the document should be replaced.
def replace?
!existing && !destroyable? && !attributes.blank?
end
# Should the document be updated?
#
# @example Should the document be updated?
# one.update?
#
# @return [ true | false ] If the object should have its attributes updated.
def update?
existing && !destroyable? && acceptable_id?
end
# Checks to see if the _id attribute (which is supposed to be
# immutable) is being asked to change. If so, raise an exception.
#
# If Mongoid::Config.immutable_ids is false, this will do nothing,
# and the update operation will fail silently.
#
# @raise [ Errors::ImmutableAttribute ] if _id has changed, and
# the document has been persisted.
def check_for_id_violation!
# look for the basic criteria of an update (see #update?)
return unless existing&.persisted? && !destroyable?
# if the id is either absent, or if it equals the existing record's
# id, there is no immutability violation.
id = extracted_id
return if existing._id == id || id.nil?
# otherwise, an attempt has been made to set the _id of an existing,
# persisted document.
if Mongoid::Config.immutable_ids
raise Errors::ImmutableAttribute.new(:_id, id)
else
Mongoid::Warnings.warn_mutable_ids
end
end
end
end
end
end