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 +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +21 -0
- data/lib/philiprehberger/dependency_graph/graph.rb +123 -0
- data/lib/philiprehberger/dependency_graph/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d893dfbce550ce9ba6022f2f647b4b70cf2306a972b52b93ed096d7bc9384c9c
|
|
4
|
+
data.tar.gz: 57ef5e2ddea71da6b58e15adeb0e1d1d38443b455d99c17aa14a795cfdf7a421
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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.
|
|
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-
|
|
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
|