mongoid/mongoid

View on GitHub
lib/mongoid/relations/proxy.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: utf-8
require "mongoid/relations/marshalable"

module Mongoid
  module Relations

    # This class is the superclass for all relation proxy objects, and contains
    # common behaviour for all of them.
    class Proxy
      alias :extend_proxy :extend

      # We undefine most methods to get them sent through to the target.
      instance_methods.each do |method|
        undef_method(method) unless
          method =~ /(^__|^send|^object_id|^respond_to|^tap|^public_send|extend_proxy|extend_proxies)/
      end

      include Threaded::Lifecycle
      include Marshalable

      attr_accessor :base, :__metadata, :target
      alias :relation_metadata :__metadata

      # Backwards compatibility with Mongoid beta releases.
      delegate :foreign_key, :inverse_foreign_key, to: :__metadata
      delegate :bind_one, :unbind_one, to: :binding
      delegate :collection_name, to: :base

      # Convenience for setting the target and the metadata properties since
      # all proxies will need to do this.
      #
      # @example Initialize the proxy.
      #   proxy.init(person, name, metadata)
      #
      # @param [ Document ] base The base document on the proxy.
      # @param [ Document, Array<Document> ] target The target of the proxy.
      # @param [ Metadata ] metadata The relation's metadata.
      #
      # @since 2.0.0.rc.1
      def init(base, target, metadata)
        @base, @target, @__metadata = base, target, metadata
        yield(self) if block_given?
        extend_proxies(metadata.extension) if metadata.extension?
      end

      # Allow extension to be an array and extend each module
      def extend_proxies(*extension)
        extension.flatten.each {|ext| extend_proxy(ext) }
      end

      # Get the class from the metadata, or return nil if no metadata present.
      #
      # @example Get the class.
      #   proxy.klass
      #
      # @return [ Class ] The relation class.
      #
      # @since 3.0.15
      def klass
        __metadata ? __metadata.klass : nil
      end

      # Resets the criteria inside the relation proxy. Used by many to many
      # relations to keep the underlying ids array in sync.
      #
      # @example Reset the relation criteria.
      #   person.preferences.reset_relation_criteria
      #
      # @since 3.0.14
      def reset_unloaded
        target.reset_unloaded(criteria)
      end

      # The default substitutable object for a relation proxy is the clone of
      # the target.
      #
      # @example Get the substitutable.
      #   proxy.substitutable
      #
      # @return [ Object ] A clone of the target.
      #
      # @since 2.1.6
      def substitutable
        target
      end

      # Tell the next persistance operation to store in a specific collection,
      # database or client.
      #
      # @example Save the current document to a different collection.
      #   model.with(collection: "secondary").save
      #
      # @example Save the current document to a different database.
      #   model.with(database: "secondary").save
      #
      # @example Save the current document to a different client.
      #   model.with(client: "replica_set").save
      #
      # @example Save with a combination of options.
      #   model.with(client: "sharded", database: "secondary").save
      #
      # @param [ Hash ] options The storage options.
      #
      # @option options [ String, Symbol ] :collection The collection name.
      # @option options [ String, Symbol ] :database The database name.
      # @option options [ String, Symbol ] :client The client name.
      #
      # @return [ Document ] The current document.
      #
      # @since 3.0.0
      def with(options)
        @persistence_options = options
        self
      end

      protected

      # Get the collection from the root of the hierarchy.
      #
      # @example Get the collection.
      #   relation.collection
      #
      # @return [ Collection ] The root's collection.
      #
      # @since 2.0.0
      def collection
        root = base._root
        root.with(@persistence_options)
        root.collection unless root.embedded?
      end

      # Takes the supplied document and sets the metadata on it.
      #
      # @example Set the metadata.
      #   proxt.characterize_one(name)
      #
      # @param [ Document ] document The document to set on.
      #
      # @since 2.0.0.rc.4
      def characterize_one(document)
        document.__metadata = __metadata unless document.__metadata
      end

      # Default behavior of method missing should be to delegate all calls
      # to the target of the proxy. This can be overridden in special cases.
      #
      # @param [ String, Symbol ] name The name of the method.
      # @param [ Array ] *args The arguments passed to the method.
      def method_missing(name, *args, &block)
        target.send(name, *args, &block)
      end

      # When the base document illegally references an embedded document this
      # error will get raised.
      #
      # @example Raise the error.
      #   relation.raise_mixed
      #
      # @raise [ Errors::MixedRelations ] The error.
      #
      # @since 2.0.0
      def raise_mixed
        raise Errors::MixedRelations.new(base.class, __metadata.klass)
      end

      # When the base is not yet saved and the user calls create or create!
      # on the relation, this error will get raised.
      #
      # @example Raise the error.
      #   relation.raise_unsaved(post)
      #
      # @param [ Document ] doc The child document getting created.
      #
      # @raise [ Errors::UnsavedDocument ] The error.
      #
      # @since 2.0.0.rc.6
      def raise_unsaved(doc)
        raise Errors::UnsavedDocument.new(base, doc)
      end

      # Return the name of defined callback method
      #
      # @example returns the before_add callback method name
      #   callback_method(:before_add)
      #
      # @param [ Symbol ] which callback
      #
      # @return [ Array ] with callback methods to be executed, the array may have symbols and Procs
      #
      # @since 3.1.0
      def callback_method(callback_name)
        methods = []
        metadata = __metadata[callback_name]
        if metadata
          if metadata.is_a?(Array)
            methods.concat(metadata)
          else
            methods << metadata
          end
        end
        methods
      end

      # Executes a callback method
      #
      # @example execute the before add callback
      #   execute_callback(:before_add)
      #
      # @param [ Symbol ] callback to be executed
      #
      # @since 3.1.0
      def execute_callback(callback, doc)
        callback_method = callback_method(callback)
        if callback_method
          callback_method.each do |c|
            if c.is_a? Proc
              c.call(base, doc)
            else
              base.send c, doc
            end
          end
        end
      end

      class << self

        # Apply ordering to the criteria if it was defined on the relation.
        #
        # @example Apply the ordering.
        #   Proxy.apply_ordering(criteria, metadata)
        #
        # @param [ Criteria ] criteria The criteria to modify.
        # @param [ Metadata ] metadata The relation metadata.
        #
        # @return [ Criteria ] The ordered criteria.
        #
        # @since 3.0.6
        def apply_ordering(criteria, metadata)
          metadata.order ? criteria.order_by(metadata.order) : criteria
        end
      end
    end
  end
end