require 'mongoid/association/constrainable' require 'mongoid/association/options' module Mongoid module Association # This module provides behaviors shared between Association types. # # @since 7.0 module Relatable include Constrainable include Options # The options shared between all association types. # # @return [ Array ] The shared options. # # @since 7.0 SHARED_OPTIONS = [ :class_name, :inverse_of, :validate, :extend ].freeze # The primary key default. # # @return [ String ] The primary key field default. # # @since 7.0 PRIMARY_KEY_DEFAULT = '_id'.freeze # The name of the association. # # @return [ Symbol ] The name of the relation. # # @since 7.0 attr_reader :name # The options on this association. # # @return [ Hash ] The options. # # @since 7.0 attr_reader :options # Initialize the Association. # # @param [ Class ] _class The class of the model who owns this relation. # @param [ Symbol ] name The name of the association. # @param [ Hash ] opts The relation options. # @param [ Block ] block The optional block. # # @since 7.0 def initialize(_class, name, opts = {}, &block) @owner_class = _class @name = name @options = opts @extension = nil create_extension!(&block) validate! end # Compare this association to another. # # @return [ Object ] The object to compare to this association. # # @since 7.0 def ==(other) relation_class_name == other.relation_class_name && inverse_class_name == other.inverse_class_name && name == other.name && options == other.options end # Get the callbacks for a given type. # # @param [ Symbol ] callback_type The type of callback type. # # @return [ Array ] A list of the callbacks, either method # names or Procs. # # @since 7.0 def get_callbacks(callback_type) Array(options[callback_type]) end # Get the type setter. # @note Only relevant for polymorphic relations that take the :as option. # # @return [ String ] The type setter method. # # @since 7.0 def type_setter @type_setter ||= type.__setter__ end # Whether trying to bind an object using this association should raise # an error. # # @param [ Document ] doc The document to be bound. # # @return [ true, false ] Whether the document can be bound. # # @since 7.0 def bindable?(doc); false; end # Get the inverse names. # # @param [ Object ] other The other model class or model object to use when # determining inverses. # # @return [ Array ] The list of inverse names. # # @since 7.0 def inverses(other = nil) return [ inverse_of ] if inverse_of if polymorphic? polymorphic_inverses(other) else determine_inverses(other) end end # Get the inverse's association metadata. # # @param [ Object ] other The other model class or model object to use when # determining inverses. # # @return [ Association ] The inverse's association metadata. # # @since 7.0 def inverse_association(other = nil) (other || relation_class).relations[inverse(other)] end # Get the inverse type. # # @return [ nil ] Default is nil for an association. # # @since 7.0 def inverse_type; end # The class name of the relation object(s). # # @return [ String ] The relation objects' class name. # # @since 7.0 def relation_class_name @class_name ||= @options[:class_name] || ActiveSupport::Inflector.classify(name) end alias :class_name :relation_class_name # The class of the relation object(s). # # @return [ String ] The relation objects' class. # # @since 7.0 def klass @klass ||= relation_class_name.constantize end alias :relation_class :klass # The class name of the object owning this relation. # # @return [ String ] The owning objects' class name. # # @since 7.0 def inverse_class_name @inverse_class_name ||= @owner_class.name end # The class of the object owning this relation. # # @return [ String ] The owning objects' class. # # @since 7.0 def inverse_class @owner_class end alias :inverse_klass :inverse_class # The foreign key field if this relation stores a foreign key. # Otherwise, the primary key. # # @return [ Symbol, String ] The primary key. # # @since 7.0 def key stores_foreign_key? ? foreign_key : primary_key end # The name of the setter on this object for assigning an associated object. # # @return [ String ] The setter name. # # @since 7.0 def setter @setter ||= "#{name}=" end # The name of the inverse setter method. # # @return [ String ] The name of the inverse setter. # # @since 7.0 def inverse_setter(other = nil) @inverse_setter ||= "#{inverses(other).first}=" unless inverses(other).blank? end # The name of the foreign key setter method. # # @return [ String ] The name of the foreign key setter. # # @since 7.0 def foreign_key_setter # note: You can't check if this association stores foreign key # See HasOne and HasMany binding, they referenced foreign_key_setter @foreign_key_setter ||= "#{foreign_key}=" if foreign_key end # The atomic path for this relation. # # @return [ Mongoid::Atomic::Paths::Root ] The atomic path object. # # @since 7.0 def path(document) relation.path(document) end # Gets the setter for the field that sets the type of document on a # polymorphic relation. # # @example Get the inverse type setter. # association.inverse_type_setter # # @return [ String ] The name of the setter. # # @since 7.0 def inverse_type_setter @inverse_type_setter ||= inverse_type.__setter__ end # Get the name of the method to check if the foreign key has changed. # # @example Get the foreign key check method. # association.foreign_key_check # # @return [ String ] The foreign key check. # # @since 7.0 def foreign_key_check @foreign_key_check ||= "#{foreign_key}_changed?" if (stores_foreign_key? && foreign_key) end # Create a relation proxy object using the owner and target. # # @param [ Document ] owner The document this relation hangs off of. # @param [ Document, Array ] target The target (parent) of the # relation. # # @return [ Proxy ] # # @since 7.0 def create_relation(owner, target) relation.new(owner, target, self) end # Whether the dependent method is destructive. # # @return [ true, false ] If the dependent method is destructive. # # @since 7.0 def destructive? @destructive ||= !!(dependent && (dependent == :delete_all || dependent == :destroy)) end # Get the counter cache column name. # # @return [ String ] The counter cache column name. # # @since 7.0 def counter_cache_column_name @counter_cache_column_name ||= (@options[:counter_cache].is_a?(String) || @options[:counter_cache].is_a?(Symbol)) ? @options[:counter_cache] : "#{inverse || inverse_class_name.demodulize.underscore.pluralize}_count" end # Get the extension. # # @return [ Module ] The extension module, if one has been defined. # # @since 7.0 def extension @extension ||= @options[:extend] end # Get the inverse name. # # @return [ Symbol ] The inverse name. # # @since 7.0 def inverse(other = nil) candidates = inverses(other) candidates.detect { |c| c } if candidates end # Whether the associated object(s) should be validated. # # @return [ true, false ] If the associated object(s) # should be validated. # # @since 7.0 def validate? @validate ||= if @options[:validate].nil? validation_default else !!@options[:validate] end end private def setup_index! @owner_class.index(index_spec, background: true) if indexed? end def define_touchable! if touchable? Association::Touchable.define_touchable!(self) end end def define_autosaver! if autosave? Association::Referenced::AutoSave.define_autosave!(self) end end def define_builder! Association::Builders.define_builder!(self) end def define_creator! Association::Builders.define_creator!(self) end def define_getter! Association::Accessors.define_getter!(self) end def define_setter! Association::Accessors.define_setter!(self) end def define_existence_check! Association::Accessors.define_existence_check!(self) end def define_ids_getter! Association::Accessors.define_ids_getter!(self) end def define_ids_setter! Association::Accessors.define_ids_setter!(self) end def define_counter_cache_callbacks! if counter_cached? Association::Referenced::CounterCache.define_callbacks!(self) end end def define_dependency! if dependent Association::Depending.define_dependency!(self) end end def validate! @options.keys.each do |opt| unless self.class::VALID_OPTIONS.include?(opt) raise Errors::InvalidRelationOption.new(@owner_class, name, opt, self.class::VALID_OPTIONS) end end [name, "#{name}?".to_sym, "#{name}=".to_sym].each do |n| if Mongoid.destructive_fields.include?(n) raise Errors::InvalidRelation.new(@owner_class, n) end end end def polymorph! if polymorphic? @owner_class.polymorphic = true end end def create_extension!(&block) if block extension_module_name = "#{@owner_class.to_s.demodulize}#{name.to_s.camelize}RelationExtension" silence_warnings do @owner_class.const_set(extension_module_name, Module.new(&block)) end @extension = "#{@owner_class}::#{extension_module_name}".constantize end end def default_inverse @default_inverse ||= klass.relations[inverse_klass.name.underscore] end end end end