Class: Modelist::PathFinder
- Inherits:
-
Object
- Object
- Modelist::PathFinder
- Defined in:
- lib/modelist/path_finder.rb
Constant Summary collapse
- DEFAULT_MAX_PATHS =
35
Class Method Summary collapse
- .back(current_node_list, counts, processed_result_partials, from) ⇒ Object
- .cache_found_path_partials(found_path_arr, cache) ⇒ Object
- .clean_underscore(classname) ⇒ Object
- .find_all(*args) ⇒ Object
- .format_result_string(*args) ⇒ Object
- .forward(current_node_list, this_class_assoc, counts, children_count) ⇒ Object
-
.get_all_paths_via_iterative_depth_first_search(from, to, directed_graph) ⇒ Object
e.g.
- .get_classname(association) ⇒ Object
- .relationship_map_excluding(exclude_name) ⇒ Object
Class Method Details
.back(current_node_list, counts, processed_result_partials, from) ⇒ Object
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/modelist/path_finder.rb', line 137 def self.back(current_node_list, counts, processed_result_partials, from) current_node_list.pop # if there is a count in the array less than two, keep removing them until they are gone while counts.last && counts.last < 2 counts.pop c = current_node_list.pop # if this is direct/indirect circular reference, don't mark unprocessed origin as processed, because it isn't unless counts.size > 1 && c.start_with?("#{from}.") # completely processed this path, so don't process it again, and don't overwrite existing success if cached #puts "marking #{c} as done" processed_result_partials[c] = [] unless processed_result_partials[c] end end # either there are no counts, or we just need to decrement the last count if counts.last && counts.last > 1 counts[counts.length-1] = counts[counts.length-1] - 1 end end |
.cache_found_path_partials(found_path_arr, cache) ⇒ Object
159 160 161 162 163 |
# File 'lib/modelist/path_finder.rb', line 159 def self.cache_found_path_partials(found_path_arr, cache) (found_path_arr.length-2).downto(0) do |i| cache[found_path_arr[i]] = found_path_arr[i+1..found_path_arr.length-1] if found_path_arr.length > 1 end end |
.clean_underscore(classname) ⇒ Object
5 6 7 8 |
# File 'lib/modelist/path_finder.rb', line 5 def self.clean_underscore(classname) classname = classname[2..classname.length] if classname.start_with?('::') classname.underscore end |
.find_all(*args) ⇒ Object
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/modelist/path_finder.rb', line 10 def self.find_all(*args) raise ArgumentError.new("Please supply a search term") unless args.size != 0 # less-dependent extract_options! = args.last.is_a?(Hash) ? args.pop : {} from = args[0] to = args[1] puts "Checking for path from #{from} to #{to}..." relations = relationship_map_excluding(to) results = get_all_paths_via_iterative_depth_first_search(from, to, relations) puts puts matching_results = results.sort_by(&:length).reverse.collect{|arr|format_result_string(arr)} #TODO: make it actually not even search past N nodes if matching_results.length > 0 puts "Paths from #{from} to #{to} (#{matching_results.length}):" # show the shortest path as the last item logged, for ease of use in CLI matching_results.each do |r| puts puts r end else puts "No path found from #{from} to #{to}." end results end |
.format_result_string(*args) ⇒ Object
165 166 167 |
# File 'lib/modelist/path_finder.rb', line 165 def self.format_result_string(*args) args.flatten.join(' -> ') end |
.forward(current_node_list, this_class_assoc, counts, children_count) ⇒ Object
132 133 134 135 |
# File 'lib/modelist/path_finder.rb', line 132 def self.forward(current_node_list, this_class_assoc, counts, children_count) current_node_list.push(this_class_assoc) counts.push(children_count) end |
.get_all_paths_via_iterative_depth_first_search(from, to, directed_graph) ⇒ Object
e.g. from = ‘model_name_1’ to = ‘model_name_2’ directed_graph = => ‘model_name_2’, ‘model_name_1.assoc_name’ => ‘model_name_3’, ‘model_name_2.assoc_name’ => ‘model_name_3’, … returns: [[‘model_name_1.assoc_name’, ‘model_name_3.assoc_name’, ‘model_name_2’], …]
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/modelist/path_finder.rb', line 71 def self.get_all_paths_via_iterative_depth_first_search(from, to, directed_graph) queue = directed_graph.keys.select {|k| k.split('.')[0] == from && directed_graph[k] != from} #puts "starting with #{queue.join(', ')}" queue.each {|k| print '+'; $stdout.flush} results = [] processed_result_partials = {} # model.assoc to array of results current_node_list = [] class_assocs_visited = [] counts = [queue.length] while queue.length > 0 #puts "queue(#{queue.length})=#{queue.inspect}" #visualize_queue(queue) this_class_assoc = queue.pop class_assocs_visited << this_class_assoc unless class_assocs_visited.include?(this_class_assoc) raise "FAILING! #{current_node_list[0].split('.')[0]} != #{from}" if current_node_list[0] && current_node_list[0].split('.')[0] != from print '-'; $stdout.flush next_class = directed_graph[this_class_assoc] #puts "processing #{this_class_assoc} => #{next_class}" current_node_list.push(this_class_assoc) #puts "current_node_list(#{current_node_list.length})=#{current_node_list.inspect}" #puts "counts: #{counts.join(',')}" step_back = true preprocessed_results = processed_result_partials[this_class_assoc] if preprocessed_results #puts "already processed #{this_class_assoc}" print '-'; $stdout.flush if preprocessed_results.length > 0 results << (current_node_list + preprocessed_results) #raise "bug in preprocessed! should start with #{from} but have result #{format_result_string(results.last)}" if current_node_list[0].split('.')[0] != from end elsif next_class == to #puts "reached #{to} in #{current_node_list.length} steps" found_path = current_node_list + [next_class] results << found_path raise "oops! should start with #{from} but have result #{format_result_string(results.last)}" if current_node_list[0].split('.')[0] != from cache_found_path_partials(found_path, processed_result_partials) elsif !current_node_list.any?{|n| n.start_with?("#{next_class}.")} children_to_visit = directed_graph.select {|k,v| k.start_with?("#{next_class}.") && directed_graph[k] != from}.keys #puts "following (#{children_to_visit.length}): #{children_to_visit.join(', ')}" children_to_visit.each {|c|print '+'; $stdout.flush} if children_to_visit.length > 0 step_back = false counts.push(children_to_visit.length) queue += children_to_visit end end back(current_node_list, counts, processed_result_partials, from) if step_back end #puts #puts #unvisited = directed_graph.keys - class_assocs_visited #puts "unvisited associations (#{unvisited.length}): #{unvisited.sort.join(', ')}" #puts results end |
.get_classname(association) ⇒ Object
169 170 171 172 173 174 175 176 |
# File 'lib/modelist/path_finder.rb', line 169 def self.get_classname(association) association.[:class_name] || case association.macro when :belongs_to, :has_one association.name.to_s when :has_and_belongs_to_many, :has_many association.name.to_s.singularize end end |
.relationship_map_excluding(exclude_name) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/modelist/path_finder.rb', line 40 def self.relationship_map_excluding(exclude_name) Rails.application.eager_load! relations = {} m_to_a = {} ActiveRecord::Base.descendants.each do |m| m_to_a[m] = m.reflect_on_all_associations end m_to_a.each do |m,as| # don't try to link via composite primary keys until that is possible, which it isn't now afaik. next if m.primary_key.is_a?(Array) as.each do |association| c1_name = clean_underscore(m.name) next if c1_name == exclude_name #TODO: we could exclude c1/key that is "to" c1 = "#{c1_name}.#{association.name}" c2 = get_classname(association) if c2 relations[c1] = clean_underscore(c2) end end unless as == nil end relations end |