Class: Rwm::DependencyGraph

Inherits:
Object
  • Object
show all
Includes:
TSort
Defined in:
lib/rwm/dependency_graph.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeDependencyGraph

Returns a new instance of DependencyGraph.



13
14
15
16
17
# File 'lib/rwm/dependency_graph.rb', line 13

def initialize
  @packages = {}   # name => Package
  @edges = {}      # name => [dep_name, ...]
  @dependents = {} # name => [dependent_name, ...]
end

Instance Attribute Details

#edgesObject (readonly)

Returns the value of attribute edges.



11
12
13
# File 'lib/rwm/dependency_graph.rb', line 11

def edges
  @edges
end

#packagesObject (readonly)

Returns the value of attribute packages.



11
12
13
# File 'lib/rwm/dependency_graph.rb', line 11

def packages
  @packages
end

Class Method Details

.build(workspace) ⇒ Object

Build graph from a workspace by parsing all Gemfiles



158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/rwm/dependency_graph.rb', line 158

def self.build(workspace)
  graph = new

  workspace.packages.each { |pkg| graph.add_package(pkg) }

  workspace.packages.each do |pkg|
    deps = GemfileParser.parse(pkg.gemfile_path, workspace.packages)
    deps.each { |dep| graph.add_edge(pkg.name, dep.name) }
  end

  graph
end

.load(workspace) ⇒ Object

Load graph from cached .rwm/graph.json, falling back to build. Auto-rebuilds when any package Gemfile is newer than the cache.



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
131
132
133
# File 'lib/rwm/dependency_graph.rb', line 93

def self.load(workspace)
  path = workspace.graph_path
  unless File.exist?(path)
    Rwm.debug("graph: no cached graph found, building from scratch")
    return build_and_save(workspace)
  end

  if stale?(path, workspace.packages)
    Rwm.debug("graph: cached graph is stale, rebuilding")
    return build_and_save(workspace)
  end

  Rwm.debug("graph: loading from cache at #{path}")
  begin
    data = JSON.parse(read_locked(path))
  rescue Errno::ENOENT
    Rwm.debug("graph: cache file disappeared, rebuilding")
    return build_and_save(workspace)
  rescue JSON::ParserError
    Rwm.debug("graph: cache file contains invalid JSON, rebuilding")
    return build_and_save(workspace)
  end

  graph = new

  workspace.packages.each { |pkg| graph.add_package(pkg) }

  data["edges"]&.each do |name, deps|
    next unless graph.packages.key?(name)

    deps.each do |dep|
      if graph.packages.key?(dep)
        graph.add_edge(name, dep)
      else
        Rwm.debug("graph: skipping stale edge #{name} -> #{dep} (package removed)")
      end
    end
  end

  graph
end

Instance Method Details

#add_edge(from_name, to_name) ⇒ Object



25
26
27
28
29
30
# File 'lib/rwm/dependency_graph.rb', line 25

def add_edge(from_name, to_name)
  @edges[from_name] ||= []
  @edges[from_name] << to_name unless @edges[from_name].include?(to_name)
  @dependents[to_name] ||= []
  @dependents[to_name] << from_name unless @dependents[to_name].include?(from_name)
end

#add_package(package) ⇒ Object



19
20
21
22
23
# File 'lib/rwm/dependency_graph.rb', line 19

def add_package(package)
  @packages[package.name] = package
  @edges[package.name] ||= []
  @dependents[package.name] ||= []
end

#dependencies(name) ⇒ Object

Dependencies of a package (what it depends on)



33
34
35
# File 'lib/rwm/dependency_graph.rb', line 33

def dependencies(name)
  @edges[name] || []
end

#direct_dependents(name) ⇒ Object

Direct dependents of a package (what depends on it)



38
39
40
# File 'lib/rwm/dependency_graph.rb', line 38

def direct_dependents(name)
  @dependents[name] || []
end

#execution_levelsObject

Group packages into execution levels — packages at the same level have no interdependencies and can run in parallel



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rwm/dependency_graph.rb', line 69

def execution_levels
  return [] if @packages.empty?

  remaining = @packages.keys.dup
  placed = Set.new
  levels = []

  until remaining.empty?
    level = remaining.select do |name|
      dependencies(name).all? { |dep| placed.include?(dep) }
    end

    raise CycleError, [["Unable to resolve execution levels — possible cycle"]] if level.empty?

    level.each { |name| placed.add(name) }
    levels << level.sort
    remaining -= level
  end

  levels
end

#save(path, workspace_root) ⇒ Object



183
184
185
186
187
188
# File 'lib/rwm/dependency_graph.rb', line 183

def save(path, workspace_root)
  @workspace_root = workspace_root
  dir = File.dirname(path)
  FileUtils.mkdir_p(dir)
  write_locked(path, JSON.pretty_generate(to_json_data) + "\n")
end

#to_dotObject



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/rwm/dependency_graph.rb', line 190

def to_dot
  lines = []
  lines << "digraph rwm {"
  lines << "  rankdir=LR;"
  lines << "  node [shape=box];"

  @packages.each_value do |pkg|
    lines << "  \"#{pkg.name}\" [label=\"#{pkg.name} (#{pkg.type})\"];"
  end

  @edges.each do |from, deps|
    deps.each do |to|
      lines << "  \"#{from}\" -> \"#{to}\";"
    end
  end

  lines << "}"
  lines.join("\n") + "\n"
end

#to_json_dataObject

Serialize to JSON for .rwm/graph.json



172
173
174
175
176
177
178
179
180
181
# File 'lib/rwm/dependency_graph.rb', line 172

def to_json_data
  {
    "version" => 1,
    "generated_at" => Time.now.iso8601,
    "packages" => @packages.transform_values do |pkg|
      { "name" => pkg.name, "type" => pkg.type.to_s, "path" => pkg.relative_path(@workspace_root || "") }
    end,
    "edges" => @edges.transform_values(&:sort)
  }
end

#to_mermaidObject



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/rwm/dependency_graph.rb', line 210

def to_mermaid
  lines = []
  lines << "graph LR"

  @packages.each_value do |pkg|
    lines << "  #{pkg.name}[\"#{pkg.name} (#{pkg.type})\"]"
  end

  @edges.each do |from, deps|
    deps.each do |to|
      lines << "  #{from} --> #{to}"
    end
  end

  lines.join("\n") + "\n"
end

#topological_orderObject

Topological sort (dependencies before dependents)



61
62
63
64
65
# File 'lib/rwm/dependency_graph.rb', line 61

def topological_order
  tsort
rescue TSort::Cyclic => e
  raise CycleError, [[e.message]]
end

#transitive_dependents(name) ⇒ Object

Walk the graph to find all transitive dependents



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/rwm/dependency_graph.rb', line 43

def transitive_dependents(name)
  visited = Set.new
  queue = [name]

  until queue.empty?
    current = queue.shift
    direct_dependents(current).each do |dep|
      next if visited.include?(dep)

      visited << dep
      queue << dep
    end
  end

  visited.to_a
end