lib/mongoid/association/proxy.rb
# frozen_string_literal: true
require 'mongoid/association/marshalable'
module Mongoid
module Association
# This class is the superclass for all association proxy objects, and contains
# common behavior for all of them.
class Proxy
extend Forwardable
alias extend_proxy extend
# specific methods to prevent from being undefined
KEEPER_METHODS = %i[
send
object_id
equal?
respond_to?
respond_to_missing?
tap
public_send
extend_proxy
extend_proxies
].freeze
# We undefine most methods to get them sent through to the target.
instance_methods.each do |method|
undef_method(method) unless method.to_s.start_with?('__') || KEEPER_METHODS.include?(method)
end
include Threaded::Lifecycle
include Marshalable
# Model instance for the base of the association.
#
# For example, if a Post embeds_many Comments, _base is a particular
# instance of the Post model.
attr_accessor :_base
attr_accessor :_association
# Model instance for one to one associations, or array of model instances
# for one to many associations, for the target of the association.
#
# For example, if a Post embeds_many Comments, _target is an array of
# Comment models embedded in a particular Post.
attr_accessor :_target
# Backwards compatibility with Mongoid beta releases.
def_delegators :_association, :foreign_key, :inverse_foreign_key
def_delegators :binding, :bind_one, :unbind_one
def_delegator :_base, :collection_name
# Sets the target and the association metadata properties.
#
# @param [ Document ] base The base document on the proxy.
# @param [ Document | Array<Document> ] target The target of the proxy.
# @param [ Mongoid::Association::Relatable ] association The association metadata.
def initialize(base, target, association)
@_base, @_target, @_association = base, target, association
yield(self) if block_given?
extend_proxies(association.extension) if association.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 association, or return nil if no association present.
#
# @example Get the class.
# proxy.klass
#
# @return [ Class ] The association class.
def klass
_association&.klass
end
# Resets the criteria inside the association proxy. Used by many to many
# associations to keep the underlying ids array in sync.
#
# @example Reset the association criteria.
# person.preferences.reset_relation_criteria
def reset_unloaded
_target.reset_unloaded(criteria)
end
# The default substitutable object for an association proxy is the clone of
# the target.
#
# @example Get the substitutable.
# proxy.substitutable
#
# @return [ Object ] A clone of the target.
def substitutable
_target
end
protected
# Get the collection from the root of the hierarchy.
#
# @example Get the collection.
# relation.collection
#
# @return [ Collection ] The root's collection.
def collection
root = _base._root
root.collection unless root.embedded?
end
# Takes the supplied document and sets the association on it.
#
# @example Set the association metadata.
# proxt.characterize_one(name)
#
# @param [ Document ] document The document to set on.
def characterize_one(document)
document._association = _association unless document._association
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 [ Object... ] *args The arguments passed to the method.
# @param &block Optional block to pass.
ruby2_keywords def method_missing(name, *args, &block)
_target.send(name, *args, &block)
end
# Whether the proxy can forward the method to the target.
#
# @param [ String | Symbol ] name The name of the method.
# @param [ Object... ] *args The +respond_to?+ arguments.
#
# @api private
ruby2_keywords def respond_to_missing?(name, *args)
_target.respond_to?(name, *args)
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.
def raise_mixed
raise Errors::MixedRelations.new(_base.class, _association.klass)
end
# When the base is not yet saved and the user calls create or create!
# on the association, 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.
def raise_unsaved(doc)
raise Errors::UnsavedDocument.new(_base, doc)
end
# Executes a callback method
#
# @example execute the before add callback
# execute_callback(:before_add)
#
# @param [ Symbol ] callback to be executed
def execute_callback(callback, doc)
_association.get_callbacks(callback).each do |c|
if c.is_a? Proc
c.call(_base, doc)
else
_base.send c, doc
end
end
end
# Execute the before and after callbacks for the given method.
#
# @param [ Symbol ] name The name of the callbacks to execute.
#
# @return [ Object ] The result of the given block
def execute_callbacks_around(name, doc)
execute_callback :"before_#{name}", doc
yield.tap do
execute_callback :"after_#{name}", doc
end
end
class << self
# Apply ordering to the criteria if it was defined on the association.
#
# @example Apply the ordering.
# Proxy.apply_ordering(criteria, association)
#
# @param [ Criteria ] criteria The criteria to modify.
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Criteria ] The ordered criteria.
def apply_ordering(criteria, association)
association.order ? criteria.order_by(association.order) : criteria
end
end
end
end
end