class Hierarchy

Public Class Methods

associations(klass) click to toggle source

Return the full hierarchy starting from the provided class

# File lib/hierarchy_tree.rb, line 6
def self.associations(klass)
  build_hierarchy(klass)
end
descendants(klass) click to toggle source

Return just the descendant classes of a provided class

# File lib/hierarchy_tree.rb, line 11
def self.descendants(klass)
  @descendants = []
  build_descendants(klass)
  @descendants
end
loop?(klass) click to toggle source
# File lib/hierarchy_tree.rb, line 17
def self.loop?(klass)
  @cache = {}
  false if dfs_hierarchy(class: klass, classes?: false)
rescue SystemStackError
  true
end

Private Class Methods

build_descendants(klass) click to toggle source
# File lib/hierarchy_tree.rb, line 81
def self.build_descendants(klass)
  dfs_descendants(class: klass, classes?: true)
rescue SystemStackError
  Rails.logger.ap "Infinite loop detected and handled for #{opts[:class]} descendants", :warn
  []
end
build_hierarchy(klass) click to toggle source
# File lib/hierarchy_tree.rb, line 26
def self.build_hierarchy(klass)
  @cache = {}
  dfs_hierarchy(class: klass)
rescue SystemStackError
  Rails.logger.ap "Infinite loop detected and handled for #{opts[:class]} hierarchy", :warn
  []
end
children_classes(opts) click to toggle source
# File lib/hierarchy_tree.rb, line 57
def self.children_classes(opts)
  walkables(opts[:class]).map do |reflection|
    child_class = get_class(reflection)
    if opts[:classes?]
      [child_class, child_class.to_s]
    else
      [child_class, reflection.name]
    end
  end.uniq
end
dfs_descendants(opts, klass_name = nil) click to toggle source
# File lib/hierarchy_tree.rb, line 88
def self.dfs_descendants(opts, klass_name = nil)
  return if klass_name.in? @descendants
  @descendants.push(klass_name) if klass_name.present?
  children_classes(opts).each do |child_klass, child_name|
    child_opts = { class: child_klass, classes?: opts[:classes?] }
    dfs_descendants(child_opts, child_name)
  end
  true
end
dfs_hierarchy(opts, klass_name = nil, ancestral_nodes = []) click to toggle source
# File lib/hierarchy_tree.rb, line 34
def self.dfs_hierarchy(opts, klass_name = nil, ancestral_nodes = [])
  return @cache[klass_name] if klass_name.in? @cache.keys
  return klass_name if opts[:class].in? ancestral_nodes # Early abort to not enter in a cycle
  if leaf?(opts[:class])
    @cache[klass_name] = klass_name
    return klass_name if klass_name.present? # Leaf
    [] # Leaf and Root
  else
    ancestral_nodes.push(opts[:class])
    children_hierarchies = children_classes(opts).map do |c_class, c_name|
      dfs_hierarchy({ class: c_class, classes?: opts[:classes?] }, c_name, ancestral_nodes.dup)
    end
    @cache[klass_name] = { klass_name => children_hierarchies }
    return @cache[klass_name] if klass_name.present? # Middle
    children_hierarchies # Root
  end
end
get_class(reflection) click to toggle source
# File lib/hierarchy_tree.rb, line 75
def self.get_class(reflection)
  child = reflection.name.to_s.singularize.classify
  child = reflection.options[:class_name].to_s if reflection.options.key?(:class_name)
  child.constantize
end
leaf?(klass) click to toggle source
# File lib/hierarchy_tree.rb, line 52
def self.leaf?(klass)
  return true if walkables(klass).empty?
  false
end
walkables(klass) click to toggle source
# File lib/hierarchy_tree.rb, line 68
def self.walkables(klass)
  # get all models associated with :has_many or :has_one that are walkable.
  klass.reflections.values.select do |r|
    r.macro.in? %i[has_one has_many] and not r.options.key?(:through)
  end
end