Skip to content

Instantly share code, notes, and snippets.

@ehrenmurdick
Last active February 5, 2024 17:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ehrenmurdick/a1686509f8381cdda06e3da14331447b to your computer and use it in GitHub Desktop.
Save ehrenmurdick/a1686509f8381cdda06e3da14331447b to your computer and use it in GitHub Desktop.

This is a tool to find association paths between different models in a rails app.

Example output - the paths from Job to Invoice:

[3] pry(main)> G.from(Job).to(Invoice)
Job.active_fleet_managed_invoice_from_swoop -- Invoice

Job.invoice >- Invoice

Job.owning_company >- Company.fleet_managed_clients -- FleetCompany.end_user_invoices -< Invoice

<snip - many more>

The edge digraphs show the arity of the associations.

  • -- one-to-one
  • >- many-to-one
  • -< one-to-many

Note: many to many (has_many :through) is shown with the join table included e.g.

Job.fleet_dispatcher >- User.invoices -< Invoice

Installation / Usage

  1. Add rgl to your Gemfile gem 'rgl', group: :delevopment
  2. Run bundle
  3. Save build_model_graph.rb to your project directory
  4. open the rails console with rails c
  5. Import the model graph file into the console load 'build_model_graph'
  6. Query the graph for the paths between two models G.from(Job).to(Invoice)
# rubocop:disable all
require 'rgl/adjacency'
require 'rgl/dijkstra'
class InteractiveGraph
attr_reader :graph
def self.get_reflections_on(model)
model.reflections.map do |name, association|
[association, model.to_s, association.klass.to_s]
rescue StandardError => e
end.compact
end
def self.each_reflection(&block)
ApplicationRecord.descendants.each do |model|
get_reflections_on(model).each(&block)
end
end
def initialize
@graph = RGL::DirectedAdjacencyGraph.new
@edge_properties = {}
@edge_weights = {}
@edge_types = {}
self.class.each_reflection do |association, from, to|
add_edge from, association.name, association.macro
add_edge association.name, to, association.macro
end
@default_edge_weights = @edge_weights.dup
end
def add_edge(from, to, edge_type = nil)
@graph.add_edge from, to
@edge_weights[[from, to]] = 1
# p edge_type
@edge_types[[from, to]] = edge_type
end
def pretty_path(path)
path.in_groups_of(2).map do |from, to|
[[from, to].compact.join('.'),
case @edge_types[[from, to]]
when :has_many
' -< '
when :belongs_to
' >- '
when nil
' '
else
' -- '
end
]
end.flatten.join
end
def inspect
"<InteractiveGraph:#{object_id}>"
end
def avoiding(dest)
@edge_weights.each do |h, k|
if k[1] == dest.to_s
@edge_weights[h] = Float::INFINITY
end
end
self
end
def reset_edge_weights
@edge_weights = @default_edge_weights.dup
end
def from(model)
reset_edge_weights
@start = model.to_s
self
end
def to(model)
reset_edge_weights
paths = []
while path = @graph.dijkstra_shortest_path(@edge_weights, @start, model.to_s)
paths.push pretty_path(path)
@edge_weights[[path[0], path[1]]] = Float::INFINITY
end
paths.reverse.each do |p|
puts p
puts
end
@start = model.to_s
self
end
end
G = InteractiveGraph.new
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment