philiprehberger-dependency_graph 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9091598b68332edaee82a0a8a55bd23d3d00278c78466b88ac86b0e25651390
4
- data.tar.gz: '0889cc1cdd1d277dc63b4c08228b6e091bacccbcb3670833d5015c4aec02cba9'
3
+ metadata.gz: d893dfbce550ce9ba6022f2f647b4b70cf2306a972b52b93ed096d7bc9384c9c
4
+ data.tar.gz: 57ef5e2ddea71da6b58e15adeb0e1d1d38443b455d99c17aa14a795cfdf7a421
5
5
  SHA512:
6
- metadata.gz: c9ecc7105f38fe9d6dcb1a63e2456d91883f26bb114b2097ea2bada656f80dfd77065c5143dadf579a9f29b07b069fc3c2fb98d39ddca7be71c65c1537275d9c
7
- data.tar.gz: 37f0dc97583b44d80f9c046dd68375fc2749ac14f360905b0de8c64e3f630645814471d012f053434b91318ec9ab77ea9f6af0b3cb839846ed760bbc31d1ef1b
6
+ metadata.gz: a3096da9a20d0a487aec73c8f5fccd26483e4ef6d19e237f767892f8ef3c5f1a8ad670d859b137b3c10c7afb4e28a86467e9485f0d081fe4bc1ca2be706eb0af
7
+ data.tar.gz: 657e23b74e72698eefd35515b25ce8f3a8d508107384460fac9d9d608589018d6b11705f49fd2547fe5d879fe5f06e68a2361d28cf2009adb5c560c6a572fef2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-21
11
+
12
+ ### Added
13
+ - `Graph#to_dot` — Graphviz DOT export for visualization
14
+
15
+ ### Fixed
16
+ - `bug_report.yml` — require Ruby version; add Gem version input per guide
17
+
18
+ ## [0.3.0] - 2026-04-09
19
+
20
+ ### Added
21
+ - `Graph#reverse` — return a new graph with all edges flipped (useful for dependent analysis)
22
+ - `Graph#all_dependents_of(item)` — transitive closure of items depending on a node
23
+ - `Graph#independent?(a, b)` — check whether two nodes are mutually unreachable
24
+
10
25
  ## [0.2.0] - 2026-04-03
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -110,6 +110,23 @@ graph.add(:a).add(:b, depends_on: [:a]).add(:c, depends_on: [:b])
110
110
  graph.resolve # => [:a, :b, :c]
111
111
  ```
112
112
 
113
+ ### Graphviz Export
114
+
115
+ ```ruby
116
+ graph = Philiprehberger::DependencyGraph.new
117
+ graph.add(:a)
118
+ graph.add(:b, depends_on: [:a])
119
+ graph.add(:c, depends_on: [:b])
120
+
121
+ puts graph.to_dot
122
+ # digraph dependencies {
123
+ # "b" -> "a";
124
+ # "c" -> "b";
125
+ # }
126
+
127
+ graph.to_dot(name: 'MyDeps') # Customize the digraph name
128
+ ```
129
+
113
130
  ## API
114
131
 
115
132
  | Method | Description |
@@ -128,6 +145,10 @@ graph.resolve # => [:a, :b, :c]
128
145
  | `Graph#roots` | Nodes with no dependencies |
129
146
  | `Graph#leaves` | Nodes with no dependents |
130
147
  | `Graph#depth(item)` | Maximum dependency depth of a node |
148
+ | `Graph#reverse` | Return a new graph with all edges flipped |
149
+ | `Graph#all_dependents_of(item)` | All transitive dependents of a node |
150
+ | `Graph#independent?(a, b)` | Whether two nodes are mutually unreachable |
151
+ | `#to_dot(name:)` | Graphviz DOT representation |
131
152
 
132
153
  ## Development
133
154
 
@@ -208,6 +208,125 @@ module Philiprehberger
208
208
  @nodes.keys.reject { |node| depended_on.include?(node) }
209
209
  end
210
210
 
211
+ # Merge another graph into this one, combining nodes and dependencies
212
+ #
213
+ # @param other [Graph] another graph to merge
214
+ # @return [self]
215
+ def merge(other)
216
+ raise Error, 'Can only merge Graph instances' unless other.is_a?(self.class)
217
+
218
+ other.nodes.each do |node, deps|
219
+ @nodes[node] ||= []
220
+ deps.each do |dep|
221
+ @nodes[node] << dep unless @nodes[node].include?(dep)
222
+ end
223
+ end
224
+ self
225
+ end
226
+
227
+ # Remove a node and all edges referencing it
228
+ #
229
+ # @param item [Object] the item to remove
230
+ # @return [Boolean] true if the node existed, false otherwise
231
+ def remove(item)
232
+ return false unless @nodes.key?(item)
233
+
234
+ @nodes.delete(item)
235
+ @nodes.each_value { |deps| deps.delete(item) }
236
+ true
237
+ end
238
+
239
+ # Total number of nodes in the graph
240
+ #
241
+ # @return [Integer]
242
+ def size
243
+ @nodes.size
244
+ end
245
+
246
+ # Whether the graph has no nodes
247
+ #
248
+ # @return [Boolean]
249
+ def empty?
250
+ @nodes.empty?
251
+ end
252
+
253
+ # Return a new graph with all edges reversed (dependents become dependencies)
254
+ #
255
+ # @return [Graph] a new graph where each edge direction is flipped
256
+ def reverse
257
+ new_graph = self.class.new
258
+ @nodes.each_key { |node| new_graph.instance_variable_get(:@nodes)[node] ||= [] }
259
+ @nodes.each do |node, deps|
260
+ deps.each { |dep| new_graph.add(dep, depends_on: [node]) }
261
+ end
262
+ new_graph
263
+ end
264
+
265
+ # Return all transitive dependents of an item (direct + indirect)
266
+ #
267
+ # @param item [Object] the item to query
268
+ # @return [Array] all items that depend on this item, directly or transitively
269
+ def all_dependents_of(item)
270
+ return [] unless @nodes.key?(item)
271
+
272
+ visited = {}
273
+ queue = dependents_of(item)
274
+ result = []
275
+
276
+ until queue.empty?
277
+ dep = queue.shift
278
+ next if visited[dep]
279
+
280
+ visited[dep] = true
281
+ result << dep
282
+ queue.concat(dependents_of(dep))
283
+ end
284
+
285
+ result
286
+ end
287
+
288
+ # Check whether two nodes are independent (neither depends on the other transitively)
289
+ #
290
+ # @param node_a [Object]
291
+ # @param node_b [Object]
292
+ # @return [Boolean] true if neither node is reachable from the other
293
+ def independent?(node_a, node_b)
294
+ return false if node_a == node_b
295
+ return false unless @nodes.key?(node_a) && @nodes.key?(node_b)
296
+
297
+ !all_dependencies_of(node_a).include?(node_b) &&
298
+ !all_dependencies_of(node_b).include?(node_a)
299
+ end
300
+
301
+ # Export the graph in Graphviz DOT format.
302
+ #
303
+ # Nodes are emitted in alphabetical order (cast to string for sort key),
304
+ # and edges within a node are sorted alphabetically for deterministic
305
+ # output. Nodes that participate in at least one edge (incoming or
306
+ # outgoing) are emitted implicitly via the edge lines; truly isolated
307
+ # nodes are declared explicitly so they still appear in the rendered
308
+ # graph. Works on graphs containing cycles.
309
+ #
310
+ # @param name [String] the digraph name used in the `digraph` header
311
+ # @return [String] Graphviz DOT source, terminated by a newline
312
+ def to_dot(name: 'dependencies')
313
+ output = "digraph #{name} {\n"
314
+ depended_on = @nodes.each_with_object({}) do |(_node, deps), acc|
315
+ deps.each { |dep| acc[dep] = true }
316
+ end
317
+ sorted_nodes = @nodes.keys.sort_by(&:to_s)
318
+ sorted_nodes.each do |node|
319
+ deps = (@nodes[node] || []).sort_by(&:to_s)
320
+ if deps.empty?
321
+ output << " #{dot_quote(node)};\n" unless depended_on[node]
322
+ else
323
+ deps.each { |dep| output << " #{dot_quote(node)} -> #{dot_quote(dep)};\n" }
324
+ end
325
+ end
326
+ output << "}\n"
327
+ output
328
+ end
329
+
211
330
  # Calculate maximum dependency depth for a node (longest path from any root to this node)
212
331
  #
213
332
  # @param item [Object] the item to query
@@ -221,6 +340,10 @@ module Philiprehberger
221
340
 
222
341
  private
223
342
 
343
+ def dot_quote(node)
344
+ %("#{node.to_s.gsub('"', '\"')}")
345
+ end
346
+
224
347
  def build_path(visited, from, to)
225
348
  path = [to]
226
349
  current = to
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module DependencyGraph
5
- VERSION = '0.2.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-dependency_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-04 00:00:00.000000000 Z
11
+ date: 2026-04-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Build and resolve dependency graphs using topological sort, detect cycles,
14
14
  generate parallel execution batches, query dependencies and dependents, find shortest