# encoding: utf-8
module Mongoid #:nodoc:
module Document
extend ActiveSupport::Concern
included do
include Mongoid::Components
cattr_accessor :_collection, :collection_name, :embedded, :primary_key, :hereditary
self.embedded = false
self.hereditary = false
self.collection_name = self.name.collectionize
attr_accessor :association_name, :_parent
attr_reader :new_record
delegate :collection, :db, :embedded, :primary_key, :to => "self.class"
end
module ClassMethods
# Return the database associated with this class.
def db
collection.db
end
# Returns the collection associated with this +Document+. If the
# document is embedded, there will be no collection associated
# with it.
#
# Returns: Mongo::Collection
def collection
raise Errors::InvalidCollection.new(self) if embedded
self._collection ||= Mongoid::Collection.new(self, self.collection_name)
add_indexes; self._collection
end
# Perform default behavior but mark the hierarchy as being hereditary.
def inherited(subclass)
super(subclass)
self.hereditary = true
end
# Instantiate a new object, only when loaded from the database or when
# the attributes have already been typecast.
#
# Example:
#
# Person.instantiate(:title => "Sir", :age => 30)
def instantiate(attrs = nil, allocating = false)
attributes = attrs || {}
if attributes["_id"] || allocating
document = allocate
document.instance_variable_set(:@attributes, attributes)
document.setup_modifications
return document
else
return new(attrs)
end
end
# Defines the field that will be used for the id of this +Document+. This
# set the id of this +Document+ before save to a parameterized version of
# the field that was supplied. This is good for use for readable URLS in
# web applications.
#
# Example:
#
# class Person
# include Mongoid::Document
# key :first_name, :last_name
# end
def key(*fields)
self.primary_key = fields
before_save :identify
end
# Macro for setting the collection name to store in.
#
# Example:
#
# Person.store_in :populdation
def store_in(name)
self.collection_name = name.to_s
self._collection = Mongoid::Collection.new(self, name.to_s)
end
# Returns all types to query for when using this class as the base.
def _types
@_type ||= (subclasses_of(self).map { |o| o.to_s } + [ self.name ])
end
# return the list of subclassses for an object
def subclasses_of(*superclasses) #:nodoc:
subclasses = []
superclasses.each do |sup|
ObjectSpace.each_object(class << sup; self; end) do |k|
if k != sup && (k.name.blank? || eval("defined?(::#{k}) && ::#{k}.object_id == k.object_id"))
subclasses << k
end
end
end
subclasses
end
end
module InstanceMethods
# Performs equality checking on the attributes. For now we chack against
# all attributes excluding timestamps on the object.
def ==(other)
return false unless other.is_a?(Document)
attributes.except(:modified_at).except(:created_at) ==
other.attributes.except(:modified_at).except(:created_at)
end
# Delegates to ==
def eql?(comparison_object)
self == (comparison_object)
end
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
id.hash
end
# Introduces a child object into the +Document+ object graph. This will
# set up the relationships between the parent and child and update the
# attributes of the parent +Document+.
#
# Options:
#
# parent: The +Document+ to assimilate with.
# options: The association +Options+ for the child.
def assimilate(parent, options)
parentize(parent, options.name); notify; self
end
# Return the attributes hash with indifferent access.
def attributes
@attributes.with_indifferent_access
end
# Clone the current +Document+. This will return all attributes with the
# exception of the document's id and versions.
def clone
self.class.instantiate(@attributes.except("_id").except("versions").dup, true)
end
# Generate an id for this +Document+.
def identify
Identity.create(self)
end
# Instantiate a new +Document+, setting the Document's attributes if
# given. If no attributes are provided, they will be initialized with
# an empty +Hash+.
#
# If a primary key is defined, the document's id will be set to that key,
# otherwise it will be set to a fresh +Mongo::ObjectID+ string.
#
# Options:
#
# attrs: The attributes +Hash+ to set up the document with.
def initialize(attrs = nil)
@attributes = {}
process(attrs)
@attributes = attributes_with_defaults(@attributes)
@new_record = true if id.nil?
document = yield self if block_given?
identify
end
# Returns the class name plus its attributes.
def inspect
attrs = fields.map { |name, field| "#{name}: #{@attributes[name].inspect}" } * ", "
"#<#{self.class.name} _id: #{id}, #{attrs}>"
end
# Notify observers of an update.
#
# Example:
#
# person.notify
def notify
notify_observers(self)
end
# Sets up a child/parent association. This is used for newly created
# objects so they can be properly added to the graph and have the parent
# observers set up properly.
#
# Options:
#
# abject: The parent object that needs to be set for the child.
# association_name: The name of the association for the child.
#
# Example:
#
# address.parentize(person, :addresses)
def parentize(object, association_name)
self._parent = object
self.association_name = association_name.to_s
add_observer(object)
end
# Return the attributes hash.
def raw_attributes
@attributes
end
# Reloads the +Document+ attributes from the database.
def reload
@attributes = collection.find_one(:_id => id)
self
end
# Remove a child document from this parent +Document+. Will reset the
# memoized association and notify the parent of the change.
def remove(child)
name = child.association_name
reset(name) { @attributes.remove(name, child.raw_attributes) }
notify
end
# Return the root +Document+ in the object graph. If the current +Document+
# is the root object in the graph it will return self.
def _root
object = self
while (object._parent) do object = object._parent; end
object || self
end
# Return an array with this +Document+ only in it.
def to_a
[ self ]
end
# Return this document as a JSON string. Nothing special is required here
# since Mongoid bubbles up all the child associations to the parent
# attribute +Hash+ using observers throughout the +Document+ lifecycle.
#
# Example:
#
# person.to_json
def to_json(options = nil)
attributes.to_json(options)
end
# Return an object to be encoded into a JSON string.
# Used by Rails 3's object->JSON chain to create JSON
# in a backend-agnostic way
#
# Example:
#
# person.as_json
def as_json(options = nil)
attributes
end
# Return this document as an object to be encoded as JSON,
# with any particular items modified on a per-encoder basis.
# Nothing special is required here since Mongoid bubbles up
# all the child associations to the parent attribute +Hash+
# using observers throughout the +Document+ lifecycle.
#
# Example:
#
# person.encode_json(encoder)
def encode_json(encoder)
attributes
end
# Returns the id of the Document, used in Rails compatibility.
def to_param
id
end
# Observe a notify call from a child +Document+. This will either update
# existing attributes on the +Document+ or clear them out for the child if
# the clear boolean is provided.
#
# Options:
#
# child: The child +Document+ that sent the notification.
# clear: Will clear out the child's attributes if set to true.
#
# This will also cause the observing +Document+ to notify it's parent if
# there is any.
def observe(child, clear = false)
name = child.association_name
attrs = child.instance_variable_get(:@attributes)
clear ? @attributes.delete(name) : @attributes.insert(name, attrs)
notify
end
protected
# apply default values to attributes - calling procs as required
def attributes_with_defaults(attributes = {})
default_values = defaults.merge(attributes)
default_values.each_pair do |key, val|
default_values[key] = val.call if val.respond_to?(:call)
end
end
end
end
end