philiprehberger-dependency_graph 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 14caf2b96dc00176fbb8c735d7e3a422ba7fa072afc920b8823cb0d8001924bb
4
+ data.tar.gz: 314339dfad0850e220045e048522d9e42576ea9152baf95436c234766d3b620d
5
+ SHA512:
6
+ metadata.gz: 109656f98b923105df821c4c1e2833dddbd44011e4b11696b6aaf956a033410ce3d879d7e632f344c38e532b4d1cfc6a0a4931097f5c20f822687c976f03ef21
7
+ data.tar.gz: fa4193c6610e9bdf3e3b7878e023cfb4fa18ddb550b5c77a45a38b710d2799e006fa50442754faf55a93c771aa8a1f44e414785dfb64d3c620b9e1631388538e
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-22
11
+
12
+ ### Added
13
+ - Initial release
14
+ - Dependency graph construction with `add` method
15
+ - Topological sort resolution via `resolve`
16
+ - Parallel batch scheduling via `parallel_batches`
17
+ - Cycle detection with `cycle?` and `cycles`
18
+ - Support for diamond and transitive dependencies
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philiprehberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # philiprehberger-dependency_graph
2
+
3
+ [![Tests](https://github.com/philiprehberger/rb-dependency-graph/actions/workflows/ci.yml/badge.svg)](https://github.com/philiprehberger/rb-dependency-graph/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/philiprehberger-dependency_graph.svg)](https://rubygems.org/gems/philiprehberger-dependency_graph)
5
+ [![License](https://img.shields.io/github/license/philiprehberger/rb-dependency-graph)](LICENSE)
6
+
7
+ Dependency resolver with topological sort and parallel batch scheduling
8
+
9
+ ## Requirements
10
+
11
+ - Ruby >= 3.1
12
+
13
+ ## Installation
14
+
15
+ Add to your Gemfile:
16
+
17
+ ```ruby
18
+ gem "philiprehberger-dependency_graph"
19
+ ```
20
+
21
+ Or install directly:
22
+
23
+ ```bash
24
+ gem install philiprehberger-dependency_graph
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```ruby
30
+ require "philiprehberger/dependency_graph"
31
+
32
+ graph = Philiprehberger::DependencyGraph.new
33
+ graph.add(:a)
34
+ graph.add(:b, depends_on: [:a])
35
+ graph.add(:c, depends_on: [:a])
36
+ graph.add(:d, depends_on: [:b, :c])
37
+
38
+ graph.resolve # => [:a, :b, :c, :d] (dependencies first)
39
+ ```
40
+
41
+ ### Parallel Batches
42
+
43
+ ```ruby
44
+ graph = Philiprehberger::DependencyGraph.new
45
+ graph.add(:a)
46
+ graph.add(:b, depends_on: [:a])
47
+ graph.add(:c, depends_on: [:a])
48
+ graph.add(:d, depends_on: [:b, :c])
49
+
50
+ graph.parallel_batches
51
+ # => [[:a], [:b, :c], [:d]]
52
+ # Batch 1: run :a
53
+ # Batch 2: run :b and :c in parallel
54
+ # Batch 3: run :d
55
+ ```
56
+
57
+ ### Cycle Detection
58
+
59
+ ```ruby
60
+ graph = Philiprehberger::DependencyGraph.new
61
+ graph.add(:a, depends_on: [:b])
62
+ graph.add(:b, depends_on: [:a])
63
+
64
+ graph.cycle? # => true
65
+ graph.cycles # => [[:a, :b, :a]]
66
+ ```
67
+
68
+ ### Chaining
69
+
70
+ ```ruby
71
+ graph = Philiprehberger::DependencyGraph.new
72
+ graph.add(:a).add(:b, depends_on: [:a]).add(:c, depends_on: [:b])
73
+ graph.resolve # => [:a, :b, :c]
74
+ ```
75
+
76
+ ## API
77
+
78
+ | Method | Description |
79
+ |--------|-------------|
80
+ | `DependencyGraph.new` | Create a new empty graph |
81
+ | `Graph#add(item, depends_on:)` | Add an item with dependencies |
82
+ | `Graph#resolve` | Topological sort, dependencies first |
83
+ | `Graph#parallel_batches` | Group into parallel execution batches |
84
+ | `Graph#cycle?` | Check if the graph contains cycles |
85
+ | `Graph#cycles` | List all detected cycles |
86
+
87
+ ## Development
88
+
89
+ ```bash
90
+ bundle install
91
+ bundle exec rspec # Run tests
92
+ bundle exec rubocop # Check code style
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module DependencyGraph
5
+ # Directed acyclic graph for dependency resolution
6
+ class Graph
7
+ Error = DependencyGraph::Error
8
+
9
+ # @return [Hash] the adjacency list (item => dependencies)
10
+ attr_reader :nodes
11
+
12
+ def initialize
13
+ @nodes = {}
14
+ end
15
+
16
+ # Add an item with its dependencies
17
+ #
18
+ # @param item [Object] the item to add
19
+ # @param depends_on [Array] the items this item depends on
20
+ # @return [self]
21
+ def add(item, depends_on: [])
22
+ @nodes[item] ||= []
23
+ depends_on.each do |dep|
24
+ @nodes[dep] ||= []
25
+ @nodes[item] << dep unless @nodes[item].include?(dep)
26
+ end
27
+ self
28
+ end
29
+
30
+ # Resolve dependencies using topological sort (Kahn's algorithm)
31
+ #
32
+ # @return [Array] items in dependency order (dependencies first)
33
+ # @raise [Error] if a cycle is detected
34
+ def resolve
35
+ raise Error, "Cycle detected: #{cycles.first.join(' -> ')}" if cycle?
36
+
37
+ in_degree = Hash.new(0)
38
+ @nodes.each_key { |node| in_degree[node] ||= 0 }
39
+ @nodes.each_value do |deps|
40
+ deps.each { |dep| in_degree[dep] += 1 }
41
+ end
42
+
43
+ queue = @nodes.keys.select { |node| in_degree[node].zero? }
44
+ result = []
45
+
46
+ until queue.empty?
47
+ node = queue.shift
48
+ result << node
49
+ @nodes[node].each do |dep|
50
+ in_degree[dep] -= 1
51
+ queue << dep if in_degree[dep].zero?
52
+ end
53
+ end
54
+
55
+ result.reverse
56
+ end
57
+
58
+ # Group items into parallel execution batches
59
+ #
60
+ # @return [Array<Array>] batches where items in each batch can run in parallel
61
+ # @raise [Error] if a cycle is detected
62
+ def parallel_batches
63
+ raise Error, "Cycle detected: #{cycles.first.join(' -> ')}" if cycle?
64
+
65
+ remaining = @nodes.keys.dup
66
+ resolved = []
67
+ batches = []
68
+
69
+ until remaining.empty?
70
+ batch = remaining.select do |item|
71
+ @nodes[item].all? { |dep| resolved.include?(dep) }
72
+ end
73
+
74
+ raise Error, 'Unable to resolve dependencies' if batch.empty?
75
+
76
+ batches << batch
77
+ resolved.concat(batch)
78
+ remaining -= batch
79
+ end
80
+
81
+ batches
82
+ end
83
+
84
+ # Check if the graph contains any cycles
85
+ #
86
+ # @return [Boolean]
87
+ def cycle?
88
+ !cycles.empty?
89
+ end
90
+
91
+ # Find all cycles in the graph
92
+ #
93
+ # @return [Array<Array>] list of cycles, each cycle is an array of items
94
+ def cycles
95
+ found_cycles = []
96
+ visited = {}
97
+ stack = {}
98
+
99
+ @nodes.each_key do |node|
100
+ next if visited[node]
101
+
102
+ detect_cycles(node, visited, stack, [], found_cycles)
103
+ end
104
+
105
+ found_cycles
106
+ end
107
+
108
+ private
109
+
110
+ def detect_cycles(node, visited, stack, path, found_cycles)
111
+ visited[node] = true
112
+ stack[node] = true
113
+ path = path + [node]
114
+
115
+ @nodes[node]&.each do |dep|
116
+ if stack[dep]
117
+ cycle_start = path.index(dep)
118
+ found_cycles << path[cycle_start..] + [dep] if cycle_start
119
+ elsif !visited[dep]
120
+ detect_cycles(dep, visited, stack, path, found_cycles)
121
+ end
122
+ end
123
+
124
+ stack.delete(node)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module DependencyGraph
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dependency_graph/version'
4
+
5
+ module Philiprehberger
6
+ module DependencyGraph
7
+ class Error < StandardError; end
8
+ end
9
+ end
10
+
11
+ require_relative 'dependency_graph/graph'
12
+
13
+ module Philiprehberger
14
+ module DependencyGraph
15
+
16
+ # Create a new dependency graph
17
+ #
18
+ # @return [Graph] a new empty graph
19
+ def self.new
20
+ Graph.new
21
+ end
22
+ end
23
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: philiprehberger-dependency_graph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Philip Rehberger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Build and resolve dependency graphs using topological sort, detect cycles,
14
+ and generate parallel execution batches for concurrent task scheduling.
15
+ email:
16
+ - me@philiprehberger.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE
23
+ - README.md
24
+ - lib/philiprehberger/dependency_graph.rb
25
+ - lib/philiprehberger/dependency_graph/graph.rb
26
+ - lib/philiprehberger/dependency_graph/version.rb
27
+ homepage: https://github.com/philiprehberger/rb-dependency-graph
28
+ licenses:
29
+ - MIT
30
+ metadata:
31
+ homepage_uri: https://github.com/philiprehberger/rb-dependency-graph
32
+ source_code_uri: https://github.com/philiprehberger/rb-dependency-graph
33
+ changelog_uri: https://github.com/philiprehberger/rb-dependency-graph/blob/main/CHANGELOG.md
34
+ bug_tracker_uri: https://github.com/philiprehberger/rb-dependency-graph/issues
35
+ rubygems_mfa_required: 'true'
36
+ post_install_message:
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 3.1.0
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.5.22
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: Dependency resolver with topological sort and parallel batch scheduling
55
+ test_files: []