mongoid/mongoid

View on GitHub
lib/mongoid/relations/builders/nested_attributes/many.rb

Summary

Maintainability
A
1 hr
Test Coverage
# encoding: utf-8
module Mongoid
  module Relations
    module Builders
      module NestedAttributes
        class Many < NestedBuilder

          # Builds the relation depending on the attributes and the options
          # passed to the macro.
          #
          # This attempts to perform 3 operations, either one of an update of
          # the existing relation, a replacement of the relation with a new
          # document, or a removal of the relation.
          #
          # @example Build the nested attrs.
          #   many.build(person)
          #
          # @param [ Document ] parent The parent document of the relation.
          #
          # @return [ Array ] The attributes.
          def build(parent, options = {})
            @existing = parent.send(metadata.name)
            if over_limit?(attributes)
              raise Errors::TooManyNestedAttributeRecords.new(existing, options[:limit])
            end
            attributes.each do |attrs|
              if attrs.is_a?(::Hash)
                process_attributes(parent, attrs.with_indifferent_access)
              else
                process_attributes(parent, attrs[1].with_indifferent_access)
              end
            end
          end

          # Create the new builder for nested attributes on one-to-many
          # relations.
          #
          # @example Initialize the builder.
          #   One.new(metadata, attributes, options)
          #
          # @param [ Metadata ] metadata The relation metadata.
          # @param [ Hash ] attributes The attributes hash to attempt to set.
          # @param [ Hash ] options The options defined.
          def initialize(metadata, attributes, options = {})
            if attributes.respond_to?(:with_indifferent_access)
              @attributes = attributes.with_indifferent_access.sort do |a, b|
                a[0].to_i <=> b[0].to_i
              end
            else
              @attributes = attributes
            end
            @metadata = metadata
            @options = options
          end

          private

          # Can the existing relation potentially be deleted?
          #
          # @example Is the document destroyable?
          #   destroyable?({ :_destroy => "1" })
          #
          # @parma [ Hash ] attributes The attributes to pull the flag from.
          #
          # @return [ true, false ] If the relation can potentially be deleted.
          def destroyable?(attributes)
            destroy = attributes.delete(:_destroy)
            [ 1, "1", true, "true" ].include?(destroy) && allow_destroy?
          end

          # Are the supplied attributes of greater number than the supplied
          # limit?
          #
          # @example Are we over the set limit?
          #   builder.over_limit?({ "street" => "Bond" })
          #
          # @param [ Hash ] attributes The attributes being set.
          #
          # @return [ true, false ] If the attributes exceed the limit.
          def over_limit?(attributes)
            limit = options[:limit]
            limit ? attributes.size > limit : false
          end

          # Process each set of attributes one at a time for each potential
          # new, existing, or ignored document.
          #
          # @api private
          #
          # @example Process the attributes
          #   builder.process_attributes({ "id" => 1, "street" => "Bond" })
          #
          # @param [ Document ] parent The parent document.
          # @param [ Hash ] attrs The single document attributes to process.
          #
          # @since 2.0.0
          def process_attributes(parent, attrs)
            return if reject?(parent, attrs)
            if id = attrs.extract_id
              first = existing.first
              converted = first ? convert_id(first.class, id) : id
              doc = existing.find(converted)
              if destroyable?(attrs)
                destroy(parent, existing, doc)
              else
                update_document(doc, attrs)
              end
            else
              existing.push(Factory.build(metadata.klass, attrs)) unless destroyable?(attrs)
            end
          end

          # Destroy the child document, needs to do some checking for embedded
          # relations and delay the destroy in case parent validation fails.
          #
          # @api private
          #
          # @example Destroy the child.
          #   builder.destroy(parent, relation, doc)
          #
          # @param [ Document ] parent The parent document.
          # @param [ Proxy ] relation The relation proxy.
          # @param [ Document ] doc The doc to destroy.
          #
          # @since 3.0.10
          def destroy(parent, relation, doc)
            doc.flagged_for_destroy = true
            if !doc.embedded? || parent.new_record?
              destroy_document(relation, doc)
            else
              parent.flagged_destroys.push(->{ destroy_document(relation, doc) })
            end
          end

          # Destroy the document.
          #
          # @api private
          #
          # @example Destroy the document.
          #   builder.destroy_document(relation, doc)
          #
          # @param [ Proxy ] relation The relation proxy.
          # @param [ Document ] doc The document to delete.
          #
          # @since 3.0.10
          def destroy_document(relation, doc)
            relation.delete(doc)
            doc.destroy unless doc.embedded? || doc.destroyed?
          end

          # Update the document.
          #
          # @api private
          #
          # @example Update the document.
          #   builder.update_document(doc, {}, options)
          #
          # @param [ Document ] doc The document to update.
          # @param [ Hash ] attrs The attributes.
          #
          # @since 3.0.10
          def update_document(doc, attrs)
            attrs.delete_id
            if metadata.embedded?
              doc.assign_attributes(attrs)
            else
              doc.update_attributes(attrs)
            end
          end
        end
      end
    end
  end
end