module Mongoid::SleepingKingStudios::HasTree::CacheAncestry

Adds ancestors and descendents methods for accessing ancestors and subtrees with a single read operation. Do not include this module directly; rather, add a :cache_ancestry => true options to the call to ::has_tree.

@example Setting up a tree with ancestry cache:

class SluggableDocument
  include Mongoid::Document
  include Mongoid::SleepingKingStudios::Tree

  has_tree :cache_ancestry => true
end # class

@since 0.5.0

Public Class Methods

apply(base, metadata) click to toggle source

@api private

Sets up the ancestry caching concern.

@param [Class] base The base class into which the concern is mixed in. @param [Hash] options The options for the relation.

@since 0.6.0

# File lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb, line 35
def self.apply base, metadata
  name = :has_tree_cache_ancestry
  validate_options name, metadata.properties

  define_fields    base, metadata
  define_accessors base, metadata
  define_helpers   base, metadata
end
define_accessors(base, metadata) click to toggle source

@api private

Overwrites the parent id setter to update the ancestor ids field, and defines the ancestors and descendents methods.

@param [Class] base The base class into which the concern is mixed in. @param [Metadata] metadata The metadata for the relation.

@since 0.6.0

# File lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb, line 53
def self.define_accessors base, metadata
  parent_id_writer = base.instance_method metadata.parent_foreign_key_writer

  base.re_define_method metadata.parent_foreign_key_writer do |value|
    old_ancestor_ids = send(metadata.foreign_key).dup

    parent_id_writer.bind(self).call value
    new_ancestor_ids = send(metadata.parent_name) ?
      send(metadata.parent_name).send(metadata.foreign_key) + [send(metadata.parent_name).id] :
      []

    descendents.each do |descendent|
      ary = descendent.send(metadata.foreign_key).dup
      ary[0..old_ancestor_ids.count] = new_ancestor_ids + [id]
      descendent.update_attributes metadata.foreign_key => ary
    end # each
    
    send :"#{metadata.foreign_key}=", new_ancestor_ids
  end # method

  base.send :define_method, metadata.relation_name do
    begin
      self.class.find(send(metadata.foreign_key))
    rescue Mongoid::Errors::DocumentNotFound, Mongoid::Errors::InvalidFind
      raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new metadata.relation_name, send(metadata.foreign_key)
    end # begin-rescue
  end # method

  base.send :define_method, :descendents do
    criteria = self.class.all

    send(metadata.foreign_key).each_with_index do |ancestor_id, index|
      criteria = criteria.where(:"#{metadata.foreign_key}.#{index}" => ancestor_id)
    end # each

    criteria.where(:"#{metadata.foreign_key}.#{send(metadata.foreign_key).count}" => id)
  end # method descendents
end
define_fields(base, metadata) click to toggle source

@api private

Defines the foreign key field on the base class.

@param [Class] base The base class into which the concern is mixed in. @param [Metadata] metadata The metadata for the relation.

@since 0.6.0

# File lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb, line 100
def self.define_fields base, metadata
  base.send :field, metadata.foreign_key, :type => Array, :default => []
end
define_helpers(base, metadata) click to toggle source

@api private

Defines the rebuild and validate ancestry helper methods.

@param [Class] base The base class into which the concern is mixed in. @param [Metadata] metadata The metadata for the relation.

@since 0.6.0

# File lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb, line 112
def self.define_helpers base, metadata
  base.send :define_method, :rebuild_ancestry! do
    begin
      ary, object = [], self
      while object.send(metadata.parent_name)
        ary.unshift object.send(metadata.parent_name).id
        object = object.send(metadata.parent_name)
      end # while
      self.send :"#{metadata.foreign_key}=", ary
    rescue Mongoid::Errors::DocumentNotFound
      raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new "#{relation_name}", object.send(metadata.parent_foreign_key)
    end # begin-rescue
  end # method rebuild_ancestry!

  base.send :define_method, :validate_ancestry! do
    return if send(metadata.foreign_key).empty?

    ancestors = []
    send(metadata.foreign_key).each_with_index do |ancestor_id, index|
      begin
        ancestor = self.class.find(ancestor_id)
        ancestors << ancestor

        if index > 0 && ancestor.send(metadata.parent_foreign_key) != send(metadata.foreign_key)[index - 1]
          # If the ancestor's parent is not the same as the previous
          # ancestor.
          raise Mongoid::SleepingKingStudios::HasTree::Errors::UnexpectedAncestor.new "#{metadata.relation_name}", ancestor.send(metadata.parent_foreign_key), send(metadata.foreign_key)[index - 1]
        end # if
      rescue Mongoid::Errors::InvalidFind, Mongoid::Errors::DocumentNotFound
        # If the ancestor id is nil, or the ancestor does not exist.
        raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new "#{metadata.relation_name}", ancestor_id
      end # begin-rescue
    end # each with index
  end # method validate_ancestry!
end
valid_options() click to toggle source

Get the valid options allowed with this concern.

@return [ Array<Symbol> ] The valid options.

@since 0.5.1

# File lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb, line 153
def self.valid_options
  %i(
    children_name # Internal; do not set directly.
    foreign_key
    parent_name   # Internal; do not set directly.
    relation_name
  ) # end Array
end