lib/mongoid/factory.rb in mongoid-8.1.7 vs lib/mongoid/factory.rb in mongoid-9.0.0
- old
+ new
@@ -1,13 +1,139 @@
# frozen_string_literal: true
module Mongoid
-
# Instantiates documents that came from the database.
module Factory
extend self
+ # A helper class for instantiating a model using either it's type
+ # class directly, or via a type class specified via a discriminator
+ # key.
+ #
+ # @api private
+ class Instantiator
+ # @return [ Mongoid::Document ] The primary model class being referenced
+ attr_reader :klass
+
+ # @return [ Hash | nil ] The Hash of attributes to use when
+ # instantiating the model.
+ attr_reader :attributes
+
+ # @return [ Mongoid::Criteria | nil ] The criteria object to
+ # use as a secondary source for the selected fields; also used when
+ # setting the inverse association.
+ attr_reader :criteria
+
+ # @return [ Array | nil ] The list of field names that should
+ # be explicitly (and exclusively) included in the new record.
+ attr_reader :selected_fields
+
+ # @return [ String | nil ] The identifier of the class that
+ # should be loaded and instantiated, in the case of a polymorphic
+ # class specification.
+ attr_reader :type
+
+ # Creates a new Factory::Initiator.
+ #
+ # @param klass [ Mongoid::Document ] The primary class to reference when
+ # instantiating the model.
+ # @param attributes [ Hash | nil ] (Optional) The hash of attributes to
+ # use when instantiating the model.
+ # @param criteria [ Mongoid::Criteria | nil ] (Optional) The criteria
+ # object to use as a secondary source for the selected fields; also
+ # used when setting the inverse association.
+ # @param selected_fields [ Array | nil ] The list of field names that
+ # should be explicitly (and exclusively) included in the new record.
+ def initialize(klass, attributes, criteria, selected_fields)
+ @klass = klass
+ @attributes = attributes
+ @criteria = criteria
+ @selected_fields = selected_fields ||
+ (criteria && criteria.options[:fields])
+ @type = attributes && attributes[klass.discriminator_key]
+ end
+
+ # Builds and returns a new instance of the requested class.
+ #
+ # @param execute_callbacks [ true | false ] Whether or not the Document
+ # callbacks should be invoked with the new instance.
+ #
+ # @raise [ Errors::UnknownModel ] when the requested type does not exist,
+ # or if it does not respond to the `instantiate` method.
+ #
+ # @return [ Mongoid::Document ] The new document instance.
+ def instance(execute_callbacks: Threaded.execute_callbacks?)
+ if type.blank?
+ instantiate_without_type(execute_callbacks)
+ else
+ instantiate_with_type(execute_callbacks)
+ end
+ end
+
+ private
+
+ # Instantiate the given class without any given subclass.
+ #
+ # @param [ true | false ] execute_callbacks Whether this method should
+ # invoke document callbacks.
+ #
+ # @return [ Document ] The instantiated document.
+ def instantiate_without_type(execute_callbacks)
+ klass.instantiate_document(attributes, selected_fields, execute_callbacks: execute_callbacks).tap do |obj|
+ if criteria&.association && criteria&.parent_document
+ obj.set_relation(criteria.association.inverse, criteria.parent_document)
+ end
+ end
+ end
+
+ # Instantiate the given `type`, which must map to another Mongoid::Document
+ # model.
+ #
+ # @param [ true | false ] execute_callbacks Whether this method should
+ # invoke document callbacks.
+ #
+ # @return [ Document ] The instantiated document.
+ def instantiate_with_type(execute_callbacks)
+ constantized_type.instantiate_document(
+ attributes, selected_fields,
+ execute_callbacks: execute_callbacks
+ )
+ end
+
+ # Retreive the `Class` instance of the requested type, either by finding it
+ # in the `klass` discriminator mapping, or by otherwise finding a
+ # Document model with the given name.
+ #
+ # @return [ Mongoid::Document ] the requested Document model
+ def constantized_type
+ @constantized_type ||= begin
+ constantized = klass.get_discriminator_mapping(type) || constantize(type)
+
+ # Check if the class is a Document class
+ raise Errors::UnknownModel.new(camelized, type) unless constantized.respond_to?(:instantiate)
+
+ constantized
+ end
+ end
+
+ # Attempts to convert the argument into a Class object by camelizing
+ # it and treating the result as the name of a constant.
+ #
+ # @param type [ String ] The name of the type to constantize
+ #
+ # @raise [ Errors::UnknownModel ] if the argument does not correspond to
+ # an existing constant.
+ #
+ # @return [ Class ] the Class that the type resolves to
+ def constantize(type)
+ camelized = type.camelize
+ camelized.constantize
+ rescue NameError
+ raise Errors::UnknownModel.new(camelized, type)
+ end
+ end
+
# Builds a new +Document+ from the supplied attributes.
#
# This method either instantiates klass or a descendant of klass if the attributes include
# klass' discriminator key.
#
@@ -18,21 +144,16 @@
# @example Build the document.
# Mongoid::Factory.build(Person, { "name" => "Durran" })
#
# @param [ Class ] klass The class to instantiate from if _type is not present.
# @param [ Hash ] attributes The document attributes.
- # @param [ true | false ] execute_callbacks Flag specifies whether callbacks
+ #
+ # @option options [ true | false ] :execute_callbacks Flag specifies whether callbacks
# should be run.
#
# @return [ Document ] The instantiated document.
def build(klass, attributes = nil)
- # A bug in Ruby 2.x (including 2.7.7) causes the attributes hash to be
- # interpreted as keyword arguments, because execute_build accepts
- # a keyword argument. Forcing an empty set of keyword arguments works
- # around the bug. Once Ruby 2.x support is dropped, this hack can be
- # removed.
- # See https://bugs.ruby-lang.org/issues/15753
execute_build(klass, attributes)
end
# Execute the build.
#
@@ -108,40 +229,13 @@
# entire object graph containing embedded associations is constructed.
#
# @return [ Document ] The instantiated document.
#
# @api private
- def execute_from_db(klass, attributes = nil, criteria = nil, selected_fields = nil, execute_callbacks: Threaded.execute_callbacks?)
- if criteria
- selected_fields ||= criteria.options[:fields]
- end
- type = (attributes || {})[klass.discriminator_key]
- if type.blank?
- obj = klass.instantiate_document(attributes, selected_fields, execute_callbacks: execute_callbacks)
- if criteria && criteria.association && criteria.parent_document
- obj.set_relation(criteria.association.inverse, criteria.parent_document)
- end
- obj
- else
- constantized = klass.get_discriminator_mapping(type)
-
- unless constantized
- camelized = type.camelize
-
- # Check if the class exists
- begin
- constantized = camelized.constantize
- rescue NameError
- raise Errors::UnknownModel.new(camelized, type)
- end
- end
-
- # Check if the class is a Document class
- if !constantized.respond_to?(:instantiate)
- raise Errors::UnknownModel.new(camelized, type)
- end
-
- constantized.instantiate_document(attributes, selected_fields, execute_callbacks: execute_callbacks)
- end
+ def execute_from_db(klass, attributes = nil, criteria = nil,
+ selected_fields = nil,
+ execute_callbacks: Threaded.execute_callbacks?)
+ Instantiator.new(klass, attributes, criteria, selected_fields)
+ .instance(execute_callbacks: execute_callbacks)
end
end
end