Class: Modelist::PathFinder
- Inherits:
-
Object
- Object
- Modelist::PathFinder
- Defined in:
- lib/modelist/path_finder.rb
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
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/modelist/path_finder.rb', line 136 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
158 159 160 161 162 |
# File 'lib/modelist/path_finder.rb', line 158 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
4 5 6 7 |
# File 'lib/modelist/path_finder.rb', line 4 def self.clean_underscore(classname) classname = classname[2..classname.length] if classname.start_with?('::') classname.underscore end |
.find_all(*args) ⇒ Object
9 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 |
# File 'lib/modelist/path_finder.rb', line 9 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
164 165 166 |
# File 'lib/modelist/path_finder.rb', line 164 def self.format_result_string(*args) args.flatten.join(' -> ') end |
.forward(current_node_list, this_class_assoc, counts, children_count) ⇒ Object
131 132 133 134 |
# File 'lib/modelist/path_finder.rb', line 131 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’], …]
70 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 |
# File 'lib/modelist/path_finder.rb', line 70 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
168 169 170 171 172 173 174 175 |
# File 'lib/modelist/path_finder.rb', line 168 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
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/modelist/path_finder.rb', line 39 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 |