module ContentfulModel::Associations::HasManyNested::ClassMethods

Class method

Public Instance Methods

has_many_nested(association_name, options = {}) click to toggle source

has_many_nested allows you to set up a tree relationship it calls has_many and belongs_to_many on the class, and sets up some methods to find a deeply-nested instance's parents

To set this up in contentful, add a multi-entry field validated to the same model as the parent, and give it a name. For example, Page might have a field called childPages:

has_many_nested :child_pages, root: -> { Page.find(“some_id”) }

would setup up an instance attribute called parent_pages which lists all the direct parents of this page. It would also create methods to find a page based on an array of its ancestors, and generate an array of ancestors. Note that this builds an array of the ancestors which called the object; because 'many' associations in Contentful are actually 'belongs_to_many' from the child end, we might have several ancestors to a page. You will need to write your own recursion for this, because it's probably an implementation-specific problem. rubocop:disable Style/PredicateName

# File lib/contentful_model/associations/has_many_nested.rb, line 27
def has_many_nested(association_name, options = {})
  has_many association_name, class_name: to_s, inverse_of: :"parent_#{has_many_nested_name}"
  belongs_to_many :"parent_#{has_many_nested_name.pluralize}", class_name: to_s
  root_method = options[:root] if options[:root].is_a?(Proc)

  # If there's a root method defined, set up a class method called root_[class name]. In our example this would be
  # Page.root_page.
  # @return [Object] the root entity returned from the proc defined in has_many_nested
  if defined?(root_method) && root_method.is_a?(Proc)
    # @return [Object] the root entity
    define_method :"root_#{has_many_nested_name}" do
      root_method.call
    end
  end

  # A utility method which returns the parent object; saves passing around interpolated strings
  define_method :parent do
    parents = send(:"parent_#{self.class.has_many_nested_name.pluralize}")
    parents.first unless parents.nil?
  end

  # Determine if the object has any parents. If it doesn't, it's considered a root.
  # This only works if the objects are called through their parents' 'child_[whatever]' method
  define_method :root? do
    parent.nil?
  end

  # Iterate over parents until you reach the root.
  # @param [Proc] a block to call on each ancestor
  # @return [Enumerable] which you can iterate over
  define_method :find_ancestors do |&block|
    return enum_for(:find_ancestors) unless block
    if parent.nil?
      # this *is* the parent
      return self
    end
    block[parent]

    parent.find_ancestors { |a| block[a] } if parent && !parent.root?
  end

  # A utility method to return the results of `find_ancestors` as an array
  # @return [Array] of ancestors in reverse order (root last)
  define_method :ancestors do
    find_ancestors.to_a
  end

  # If this entry is the root, return self.
  # Otherwise, return the last member of the ancestors, which is the root
  # @return the root instance of this object
  define_method :root do
    return self if root?

    find_ancestors.to_a.last
  end

  # @return [Boolean] whether or not this instance has children
  define_method :children? do
    !send(association_name).empty?
  end

  # @return [Array] a collection of child objects, based on the association name
  define_method :children do
    send(association_name)
  end

  # @return [Hash] a hash of nested child objects
  define_method :nested_children do
    children.each_with_object({}) do |e, a|
      children = e.children? ? e.nested_children : nil
      a[e] = children
    end
  end

  # Return a nested hash of children, returning the field specified
  # @param field [Symbol] the field you want to return, nested for each child
  # @return [Hash] of nested children, by that field
  define_method :nested_children_by do |field|
    children.each_with_object({}) do |e, a|
      children = e.children? ? e.nested_children_by(field) : nil
      a[e.send(field)] = children
    end
  end

  # Return a flattened hash of children by the specified field
  define_method :all_child_paths_by do |field, opts = {}|
    options = { prefix: nil }.merge!(opts)
    flatten_hash(nested_children_by(field)).keys.collect do |path|
      options[:prefix] ? path.unshift(options[:prefix]) : path
    end
  end

  # Search for a child by a certain field. This is called on the parent(s).
  # e.g. Page.root.find_child_path_by(:slug, "some-slug"). Accepts a prefix if you want to
  # prefix the children with the parent
  define_method :find_child_path_by do |field, value, opts = {}|
    all_child_paths_by(field, opts).select { |child| child.include?(value) }
  end

  # Private method to flatten a hash. Courtesy Cary Swoveland http://stackoverflow.com/a/23861946
  define_method :flatten_hash do |h, f = [], g = {}|
    return g.update(f => h) unless h.is_a? Hash
    h.each { |k, r| flatten_hash(r, f + [k], g) }
    g
  end
  send(:private, :flatten_hash)
end
has_many_nested_name() click to toggle source
# File lib/contentful_model/associations/has_many_nested.rb, line 135
def has_many_nested_name
  to_s.demodulize.underscore
end