philiprehberger-dependency_graph 0.1.7 → 0.2.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 +17 -0
- data/README.md +45 -0
- data/lib/philiprehberger/dependency_graph/graph.rb +137 -0
- data/lib/philiprehberger/dependency_graph/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9091598b68332edaee82a0a8a55bd23d3d00278c78466b88ac86b0e25651390
|
|
4
|
+
data.tar.gz: '0889cc1cdd1d277dc63b4c08228b6e091bacccbcb3670833d5015c4aec02cba9'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9ecc7105f38fe9d6dcb1a63e2456d91883f26bb114b2097ea2bada656f80dfd77065c5143dadf579a9f29b07b069fc3c2fb98d39ddca7be71c65c1537275d9c
|
|
7
|
+
data.tar.gz: 37f0dc97583b44d80f9c046dd68375fc2749ac14f360905b0de8c64e3f630645814471d012f053434b91318ec9ab77ea9f6af0b3cb839846ed760bbc31d1ef1b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-04-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Graph#dependencies_of(item)` — returns direct dependencies of a node
|
|
14
|
+
- `Graph#all_dependencies_of(item)` — returns transitive closure of all dependencies
|
|
15
|
+
- `Graph#dependents_of(item)` — reverse lookup of items that depend on a node
|
|
16
|
+
- `Graph#path(from, to)` — BFS shortest dependency path between two nodes
|
|
17
|
+
- `Graph#subgraph(*items)` — extract a new graph containing only specified nodes and their edges
|
|
18
|
+
- `Graph#roots` — nodes with no dependencies
|
|
19
|
+
- `Graph#leaves` — nodes with no dependents
|
|
20
|
+
- `Graph#depth(item)` — maximum dependency depth for a node
|
|
21
|
+
|
|
22
|
+
## [0.1.8] - 2026-03-31
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
26
|
+
|
|
10
27
|
## [0.1.7] - 2026-03-31
|
|
11
28
|
|
|
12
29
|
### Changed
|
data/README.md
CHANGED
|
@@ -65,6 +65,43 @@ graph.cycle? # => true
|
|
|
65
65
|
graph.cycles # => [[:a, :b, :a]]
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Dependency Queries
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
graph = Philiprehberger::DependencyGraph.new
|
|
72
|
+
graph.add(:a)
|
|
73
|
+
graph.add(:b, depends_on: [:a])
|
|
74
|
+
graph.add(:c, depends_on: [:b])
|
|
75
|
+
graph.add(:d, depends_on: [:b, :c])
|
|
76
|
+
|
|
77
|
+
graph.dependencies_of(:d) # => [:b, :c]
|
|
78
|
+
graph.all_dependencies_of(:d) # => [:b, :c, :a]
|
|
79
|
+
graph.dependents_of(:b) # => [:c, :d]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Path Finding
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
graph.path(:d, :a) # => [:d, :b, :a]
|
|
86
|
+
graph.path(:a, :d) # => nil (no path in that direction)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Subgraph Extraction
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
sub = graph.subgraph(:a, :b, :c)
|
|
93
|
+
sub.resolve # => [:a, :b, :c]
|
|
94
|
+
# Edges to nodes outside the subgraph are excluded
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Roots, Leaves, and Depth
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
graph.roots # => [:a] (no dependencies)
|
|
101
|
+
graph.leaves # => [:d] (nothing depends on it)
|
|
102
|
+
graph.depth(:d) # => 2 (longest path from a root)
|
|
103
|
+
```
|
|
104
|
+
|
|
68
105
|
### Chaining
|
|
69
106
|
|
|
70
107
|
```ruby
|
|
@@ -83,6 +120,14 @@ graph.resolve # => [:a, :b, :c]
|
|
|
83
120
|
| `Graph#parallel_batches` | Group into parallel execution batches |
|
|
84
121
|
| `Graph#cycle?` | Check if the graph contains cycles |
|
|
85
122
|
| `Graph#cycles` | List all detected cycles |
|
|
123
|
+
| `Graph#dependencies_of(item)` | Direct dependencies of an item |
|
|
124
|
+
| `Graph#all_dependencies_of(item)` | All transitive dependencies |
|
|
125
|
+
| `Graph#dependents_of(item)` | Items that directly depend on an item |
|
|
126
|
+
| `Graph#path(from, to)` | Shortest dependency path (BFS), or nil |
|
|
127
|
+
| `Graph#subgraph(*items)` | Extract a new graph with specified nodes |
|
|
128
|
+
| `Graph#roots` | Nodes with no dependencies |
|
|
129
|
+
| `Graph#leaves` | Nodes with no dependents |
|
|
130
|
+
| `Graph#depth(item)` | Maximum dependency depth of a node |
|
|
86
131
|
|
|
87
132
|
## Development
|
|
88
133
|
|
|
@@ -105,8 +105,145 @@ module Philiprehberger
|
|
|
105
105
|
found_cycles
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Return direct dependencies of an item
|
|
109
|
+
#
|
|
110
|
+
# @param item [Object] the item to query
|
|
111
|
+
# @return [Array] direct dependencies, or empty array if item is unknown
|
|
112
|
+
def dependencies_of(item)
|
|
113
|
+
(@nodes[item] || []).dup
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Return all transitive dependencies of an item (direct + indirect)
|
|
117
|
+
#
|
|
118
|
+
# @param item [Object] the item to query
|
|
119
|
+
# @return [Array] all dependencies in no particular order
|
|
120
|
+
def all_dependencies_of(item)
|
|
121
|
+
return [] unless @nodes.key?(item)
|
|
122
|
+
|
|
123
|
+
visited = {}
|
|
124
|
+
queue = @nodes[item].dup
|
|
125
|
+
result = []
|
|
126
|
+
|
|
127
|
+
until queue.empty?
|
|
128
|
+
dep = queue.shift
|
|
129
|
+
next if visited[dep]
|
|
130
|
+
|
|
131
|
+
visited[dep] = true
|
|
132
|
+
result << dep
|
|
133
|
+
queue.concat(@nodes[dep] || [])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Return items that directly depend on the given item (reverse lookup)
|
|
140
|
+
#
|
|
141
|
+
# @param item [Object] the item to query
|
|
142
|
+
# @return [Array] direct dependents
|
|
143
|
+
def dependents_of(item)
|
|
144
|
+
@nodes.each_with_object([]) do |(node, deps), acc|
|
|
145
|
+
acc << node if deps.include?(item)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Find shortest dependency path between two nodes using BFS
|
|
150
|
+
#
|
|
151
|
+
# @param from [Object] the starting node
|
|
152
|
+
# @param to [Object] the target node
|
|
153
|
+
# @return [Array, nil] array of nodes forming the path, or nil if no path exists
|
|
154
|
+
def path(from, to)
|
|
155
|
+
return nil unless @nodes.key?(from) && @nodes.key?(to)
|
|
156
|
+
return [from] if from == to
|
|
157
|
+
|
|
158
|
+
visited = { from => nil }
|
|
159
|
+
queue = [from]
|
|
160
|
+
|
|
161
|
+
until queue.empty?
|
|
162
|
+
current = queue.shift
|
|
163
|
+
(@nodes[current] || []).each do |dep|
|
|
164
|
+
next if visited.key?(dep)
|
|
165
|
+
|
|
166
|
+
visited[dep] = current
|
|
167
|
+
if dep == to
|
|
168
|
+
return build_path(visited, from, to)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
queue << dep
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Extract a subgraph containing only the specified nodes and edges between them
|
|
179
|
+
#
|
|
180
|
+
# @param items [Array<Object>] nodes to include
|
|
181
|
+
# @return [Graph] a new graph with only the specified nodes
|
|
182
|
+
def subgraph(*items)
|
|
183
|
+
item_set = items.flatten.to_h { |i| [i, true] }
|
|
184
|
+
new_graph = self.class.new
|
|
185
|
+
|
|
186
|
+
items.flatten.each do |item|
|
|
187
|
+
next unless @nodes.key?(item)
|
|
188
|
+
|
|
189
|
+
matching_deps = (@nodes[item] || []).select { |dep| item_set[dep] }
|
|
190
|
+
new_graph.add(item, depends_on: matching_deps)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
new_graph
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Return nodes that have no dependencies
|
|
197
|
+
#
|
|
198
|
+
# @return [Array] root nodes
|
|
199
|
+
def roots
|
|
200
|
+
@nodes.select { |_node, deps| deps.empty? }.keys
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Return nodes with no dependents (no other node depends on them)
|
|
204
|
+
#
|
|
205
|
+
# @return [Array] leaf nodes
|
|
206
|
+
def leaves
|
|
207
|
+
depended_on = @nodes.values.flatten.uniq
|
|
208
|
+
@nodes.keys.reject { |node| depended_on.include?(node) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Calculate maximum dependency depth for a node (longest path from any root to this node)
|
|
212
|
+
#
|
|
213
|
+
# @param item [Object] the item to query
|
|
214
|
+
# @return [Integer] the depth, or 0 if the item is a root or unknown
|
|
215
|
+
def depth(item)
|
|
216
|
+
return 0 unless @nodes.key?(item)
|
|
217
|
+
|
|
218
|
+
memo = {}
|
|
219
|
+
compute_depth(item, memo)
|
|
220
|
+
end
|
|
221
|
+
|
|
108
222
|
private
|
|
109
223
|
|
|
224
|
+
def build_path(visited, from, to)
|
|
225
|
+
path = [to]
|
|
226
|
+
current = to
|
|
227
|
+
while current != from
|
|
228
|
+
current = visited[current]
|
|
229
|
+
path.unshift(current)
|
|
230
|
+
end
|
|
231
|
+
path
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def compute_depth(item, memo)
|
|
235
|
+
return memo[item] if memo.key?(item)
|
|
236
|
+
|
|
237
|
+
deps = @nodes[item] || []
|
|
238
|
+
memo[item] = if deps.empty?
|
|
239
|
+
0
|
|
240
|
+
else
|
|
241
|
+
deps.map { |dep| compute_depth(dep, memo) }.max + 1
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
memo[item]
|
|
245
|
+
end
|
|
246
|
+
|
|
110
247
|
def detect_cycles(node, visited, stack, path, found_cycles)
|
|
111
248
|
visited[node] = true
|
|
112
249
|
stack[node] = true
|
metadata
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
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.2.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-
|
|
11
|
+
date: 2026-04-04 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
|
|
15
|
+
paths, and extract subgraphs.
|
|
15
16
|
email:
|
|
16
17
|
- me@philiprehberger.com
|
|
17
18
|
executables: []
|
|
@@ -24,11 +25,11 @@ files:
|
|
|
24
25
|
- lib/philiprehberger/dependency_graph.rb
|
|
25
26
|
- lib/philiprehberger/dependency_graph/graph.rb
|
|
26
27
|
- lib/philiprehberger/dependency_graph/version.rb
|
|
27
|
-
homepage: https://
|
|
28
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-dependency_graph
|
|
28
29
|
licenses:
|
|
29
30
|
- MIT
|
|
30
31
|
metadata:
|
|
31
|
-
homepage_uri: https://
|
|
32
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-dependency_graph
|
|
32
33
|
source_code_uri: https://github.com/philiprehberger/rb-dependency-graph
|
|
33
34
|
changelog_uri: https://github.com/philiprehberger/rb-dependency-graph/blob/main/CHANGELOG.md
|
|
34
35
|
bug_tracker_uri: https://github.com/philiprehberger/rb-dependency-graph/issues
|