spikard 0.3.2 → 0.3.4
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/LICENSE +1 -1
- data/README.md +659 -659
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +386 -386
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +221 -221
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +360 -360
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +63 -63
- data/vendor/crates/spikard-core/src/di/container.rs +726 -726
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/crates/spikard-core/src/di/value.rs +283 -283
- data/vendor/crates/spikard-core/src/errors.rs +39 -39
- data/vendor/crates/spikard-core/src/http.rs +153 -153
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/crates/spikard-core/src/parameters.rs +722 -722
- data/vendor/crates/spikard-core/src/problem.rs +310 -310
- data/vendor/crates/spikard-core/src/request_data.rs +189 -189
- data/vendor/crates/spikard-core/src/router.rs +249 -249
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation.rs +699 -699
- data/vendor/crates/spikard-http/Cargo.toml +58 -58
- data/vendor/crates/spikard-http/src/auth.rs +247 -247
- data/vendor/crates/spikard-http/src/background.rs +249 -249
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +490 -490
- data/vendor/crates/spikard-http/src/debug.rs +63 -63
- data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/crates/spikard-http/src/lib.rs +529 -529
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/crates/spikard-http/src/parameters.rs +1 -1
- data/vendor/crates/spikard-http/src/problem.rs +1 -1
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
- data/vendor/crates/spikard-http/src/response.rs +399 -399
- data/vendor/crates/spikard-http/src/router.rs +1 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/crates/spikard-http/src/sse.rs +447 -447
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/crates/spikard-http/src/testing.rs +377 -377
- data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
- data/vendor/crates/spikard-http/src/validation.rs +1 -1
- data/vendor/crates/spikard-http/src/websocket.rs +324 -324
- data/vendor/crates/spikard-rb/Cargo.toml +42 -42
- data/vendor/crates/spikard-rb/build.rs +8 -8
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config.rs +294 -294
- data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
- data/vendor/crates/spikard-rb/src/di.rs +409 -409
- data/vendor/crates/spikard-rb/src/handler.rs +625 -625
- data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
- data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
- data/vendor/crates/spikard-rb/src/server.rs +283 -283
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
- data/vendor/spikard-core/Cargo.toml +40 -40
- data/vendor/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/spikard-core/src/debug.rs +63 -63
- data/vendor/spikard-core/src/di/container.rs +726 -726
- data/vendor/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/spikard-core/src/di/error.rs +118 -118
- data/vendor/spikard-core/src/di/factory.rs +538 -538
- data/vendor/spikard-core/src/di/graph.rs +545 -545
- data/vendor/spikard-core/src/di/mod.rs +192 -192
- data/vendor/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/spikard-core/src/di/value.rs +283 -283
- data/vendor/spikard-core/src/http.rs +153 -153
- data/vendor/spikard-core/src/lib.rs +28 -28
- data/vendor/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/spikard-core/src/parameters.rs +719 -719
- data/vendor/spikard-core/src/problem.rs +310 -310
- data/vendor/spikard-core/src/request_data.rs +189 -189
- data/vendor/spikard-core/src/router.rs +249 -249
- data/vendor/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/spikard-core/src/type_hints.rs +304 -304
- data/vendor/spikard-core/src/validation.rs +699 -699
- data/vendor/spikard-http/Cargo.toml +58 -58
- data/vendor/spikard-http/src/auth.rs +247 -247
- data/vendor/spikard-http/src/background.rs +249 -249
- data/vendor/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/spikard-http/src/cors.rs +490 -490
- data/vendor/spikard-http/src/debug.rs +63 -63
- data/vendor/spikard-http/src/di_handler.rs +423 -423
- data/vendor/spikard-http/src/handler_response.rs +190 -190
- data/vendor/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/spikard-http/src/lib.rs +529 -529
- data/vendor/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/spikard-http/src/parameters.rs +1 -1
- data/vendor/spikard-http/src/problem.rs +1 -1
- data/vendor/spikard-http/src/query_parser.rs +369 -369
- data/vendor/spikard-http/src/response.rs +399 -399
- data/vendor/spikard-http/src/router.rs +1 -1
- data/vendor/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/spikard-http/src/server/handler.rs +80 -80
- data/vendor/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/spikard-http/src/server/mod.rs +805 -805
- data/vendor/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/spikard-http/src/sse.rs +447 -447
- data/vendor/spikard-http/src/testing/form.rs +14 -14
- data/vendor/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/spikard-http/src/testing.rs +377 -377
- data/vendor/spikard-http/src/type_hints.rs +1 -1
- data/vendor/spikard-http/src/validation.rs +1 -1
- data/vendor/spikard-http/src/websocket.rs +324 -324
- data/vendor/spikard-rb/Cargo.toml +42 -42
- data/vendor/spikard-rb/build.rs +8 -8
- data/vendor/spikard-rb/src/background.rs +63 -63
- data/vendor/spikard-rb/src/config.rs +294 -294
- data/vendor/spikard-rb/src/conversion.rs +392 -392
- data/vendor/spikard-rb/src/di.rs +409 -409
- data/vendor/spikard-rb/src/handler.rs +534 -534
- data/vendor/spikard-rb/src/lib.rs +2020 -2020
- data/vendor/spikard-rb/src/lifecycle.rs +267 -267
- data/vendor/spikard-rb/src/server.rs +283 -283
- data/vendor/spikard-rb/src/sse.rs +231 -231
- data/vendor/spikard-rb/src/test_client.rs +404 -404
- data/vendor/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/spikard-rb/src/websocket.rs +233 -233
- metadata +1 -1
|
@@ -1,545 +1,545 @@
|
|
|
1
|
-
//! Dependency graph with topological sorting and cycle detection
|
|
2
|
-
//!
|
|
3
|
-
//! This module provides the `DependencyGraph` type which manages the dependency
|
|
4
|
-
//! relationships between registered dependencies, detects cycles, and calculates
|
|
5
|
-
//! the optimal batched resolution order.
|
|
6
|
-
|
|
7
|
-
use super::error::DependencyError;
|
|
8
|
-
use std::collections::{HashMap, HashSet, VecDeque};
|
|
9
|
-
|
|
10
|
-
/// Dependency graph for managing dependency relationships
|
|
11
|
-
///
|
|
12
|
-
/// The graph tracks which dependencies depend on which other dependencies,
|
|
13
|
-
/// and provides algorithms for:
|
|
14
|
-
/// - Cycle detection (preventing circular dependencies)
|
|
15
|
-
/// - Topological sorting (determining resolution order)
|
|
16
|
-
/// - Batch calculation (enabling parallel resolution)
|
|
17
|
-
///
|
|
18
|
-
/// # Examples
|
|
19
|
-
///
|
|
20
|
-
/// ```ignore
|
|
21
|
-
/// use spikard_core::di::DependencyGraph;
|
|
22
|
-
///
|
|
23
|
-
/// let mut graph = DependencyGraph::new();
|
|
24
|
-
///
|
|
25
|
-
/// // Add dependencies
|
|
26
|
-
/// graph.add_dependency("config", vec![]).unwrap();
|
|
27
|
-
/// graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
28
|
-
/// graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
29
|
-
/// graph.add_dependency("service", vec!["database".to_string(), "cache".to_string()]).unwrap();
|
|
30
|
-
///
|
|
31
|
-
/// // Calculate batches for parallel resolution
|
|
32
|
-
/// let batches = graph.calculate_batches(&[
|
|
33
|
-
/// "config".to_string(),
|
|
34
|
-
/// "database".to_string(),
|
|
35
|
-
/// "cache".to_string(),
|
|
36
|
-
/// "service".to_string(),
|
|
37
|
-
/// ]).unwrap();
|
|
38
|
-
///
|
|
39
|
-
/// // Batch 1: config (no dependencies)
|
|
40
|
-
/// // Batch 2: database, cache (both depend only on config, can run in parallel)
|
|
41
|
-
/// // Batch 3: service (depends on database and cache)
|
|
42
|
-
/// assert_eq!(batches.len(), 3);
|
|
43
|
-
/// ```
|
|
44
|
-
#[derive(Debug, Clone, Default)]
|
|
45
|
-
pub struct DependencyGraph {
|
|
46
|
-
/// Adjacency list: key -> list of dependencies it depends on
|
|
47
|
-
graph: HashMap<String, Vec<String>>,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
impl DependencyGraph {
|
|
51
|
-
/// Create a new empty dependency graph
|
|
52
|
-
///
|
|
53
|
-
/// # Examples
|
|
54
|
-
///
|
|
55
|
-
/// ```ignore
|
|
56
|
-
/// use spikard_core::di::DependencyGraph;
|
|
57
|
-
///
|
|
58
|
-
/// let graph = DependencyGraph::new();
|
|
59
|
-
/// ```
|
|
60
|
-
#[must_use]
|
|
61
|
-
pub fn new() -> Self {
|
|
62
|
-
Self { graph: HashMap::new() }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// Add a dependency and its dependencies to the graph
|
|
66
|
-
///
|
|
67
|
-
/// This will check for cycles before adding. If adding this dependency
|
|
68
|
-
/// would create a cycle, it returns an error.
|
|
69
|
-
///
|
|
70
|
-
/// # Arguments
|
|
71
|
-
///
|
|
72
|
-
/// * `key` - The unique key for this dependency
|
|
73
|
-
/// * `depends_on` - List of dependency keys that this depends on
|
|
74
|
-
///
|
|
75
|
-
/// # Errors
|
|
76
|
-
///
|
|
77
|
-
/// Returns `DependencyError::CircularDependency` if adding this dependency
|
|
78
|
-
/// would create a cycle in the graph.
|
|
79
|
-
///
|
|
80
|
-
/// Returns `DependencyError::DuplicateKey` if a dependency with this key
|
|
81
|
-
/// already exists.
|
|
82
|
-
///
|
|
83
|
-
/// # Examples
|
|
84
|
-
///
|
|
85
|
-
/// ```ignore
|
|
86
|
-
/// use spikard_core::di::DependencyGraph;
|
|
87
|
-
///
|
|
88
|
-
/// let mut graph = DependencyGraph::new();
|
|
89
|
-
///
|
|
90
|
-
/// // Simple dependency chain
|
|
91
|
-
/// graph.add_dependency("a", vec![]).unwrap();
|
|
92
|
-
/// graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
93
|
-
///
|
|
94
|
-
/// // This would create a cycle: a -> b -> a
|
|
95
|
-
/// let result = graph.add_dependency("a", vec!["b".to_string()]);
|
|
96
|
-
/// assert!(result.is_err());
|
|
97
|
-
/// ```
|
|
98
|
-
pub fn add_dependency(&mut self, key: &str, depends_on: Vec<String>) -> Result<(), DependencyError> {
|
|
99
|
-
// Check for duplicate
|
|
100
|
-
if self.graph.contains_key(key) {
|
|
101
|
-
return Err(DependencyError::DuplicateKey { key: key.to_string() });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Don't check for cycles here - allow registration and detect at resolution time
|
|
105
|
-
// This allows the server to start and return proper HTTP error responses
|
|
106
|
-
self.graph.insert(key.to_string(), depends_on);
|
|
107
|
-
Ok(())
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/// Check if adding a new dependency would create a cycle
|
|
111
|
-
///
|
|
112
|
-
/// Uses depth-first search to detect cycles in the graph if the new
|
|
113
|
-
/// dependency were added.
|
|
114
|
-
///
|
|
115
|
-
/// # Arguments
|
|
116
|
-
///
|
|
117
|
-
/// * `new_key` - The key of the dependency to potentially add
|
|
118
|
-
/// * `new_deps` - The dependencies that the new dependency would depend on
|
|
119
|
-
///
|
|
120
|
-
/// # Returns
|
|
121
|
-
///
|
|
122
|
-
/// `true` if adding this dependency would create a cycle, `false` otherwise.
|
|
123
|
-
///
|
|
124
|
-
/// # Examples
|
|
125
|
-
///
|
|
126
|
-
/// ```ignore
|
|
127
|
-
/// use spikard_core::di::DependencyGraph;
|
|
128
|
-
///
|
|
129
|
-
/// let mut graph = DependencyGraph::new();
|
|
130
|
-
/// graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
131
|
-
/// graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
132
|
-
///
|
|
133
|
-
/// // Adding c -> a would create a cycle
|
|
134
|
-
/// assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
135
|
-
///
|
|
136
|
-
/// // Adding c -> [] would not
|
|
137
|
-
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
138
|
-
/// ```
|
|
139
|
-
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
140
|
-
// Build temporary graph with the new dependency
|
|
141
|
-
let mut temp_graph = self.graph.clone();
|
|
142
|
-
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|
|
143
|
-
|
|
144
|
-
// DFS cycle detection
|
|
145
|
-
let mut visited = HashSet::new();
|
|
146
|
-
let mut rec_stack = HashSet::new();
|
|
147
|
-
|
|
148
|
-
for key in temp_graph.keys() {
|
|
149
|
-
if !visited.contains(key) && Self::has_cycle_dfs(key, &temp_graph, &mut visited, &mut rec_stack) {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
false
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/// Depth-first search for cycle detection
|
|
158
|
-
///
|
|
159
|
-
/// # Arguments
|
|
160
|
-
///
|
|
161
|
-
/// * `node` - Current node being visited
|
|
162
|
-
/// * `graph` - The graph to search
|
|
163
|
-
/// * `visited` - Set of all visited nodes
|
|
164
|
-
/// * `rec_stack` - Set of nodes in the current recursion stack
|
|
165
|
-
///
|
|
166
|
-
/// # Returns
|
|
167
|
-
///
|
|
168
|
-
/// `true` if a cycle is detected, `false` otherwise.
|
|
169
|
-
fn has_cycle_dfs(
|
|
170
|
-
node: &str,
|
|
171
|
-
graph: &HashMap<String, Vec<String>>,
|
|
172
|
-
visited: &mut HashSet<String>,
|
|
173
|
-
rec_stack: &mut HashSet<String>,
|
|
174
|
-
) -> bool {
|
|
175
|
-
visited.insert(node.to_string());
|
|
176
|
-
rec_stack.insert(node.to_string());
|
|
177
|
-
|
|
178
|
-
if let Some(deps) = graph.get(node) {
|
|
179
|
-
for dep in deps {
|
|
180
|
-
if !visited.contains(dep) {
|
|
181
|
-
if Self::has_cycle_dfs(dep, graph, visited, rec_stack) {
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
} else if rec_stack.contains(dep) {
|
|
185
|
-
// Found a back edge (cycle)
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
rec_stack.remove(node);
|
|
192
|
-
false
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/// Calculate batches of dependencies that can be resolved in parallel
|
|
196
|
-
///
|
|
197
|
-
/// Uses topological sorting with Kahn's algorithm to determine the order
|
|
198
|
-
/// in which dependencies should be resolved. Dependencies with no unresolved
|
|
199
|
-
/// dependencies can be resolved in parallel (same batch).
|
|
200
|
-
///
|
|
201
|
-
/// # Arguments
|
|
202
|
-
///
|
|
203
|
-
/// * `keys` - The dependency keys to resolve
|
|
204
|
-
///
|
|
205
|
-
/// # Returns
|
|
206
|
-
///
|
|
207
|
-
/// A vector of batches, where each batch is a set of dependency keys that
|
|
208
|
-
/// can be resolved in parallel. Batches must be executed sequentially.
|
|
209
|
-
///
|
|
210
|
-
/// # Errors
|
|
211
|
-
///
|
|
212
|
-
/// Returns `DependencyError::CircularDependency` if the graph contains a cycle.
|
|
213
|
-
/// Returns `DependencyError::NotFound` if a requested dependency doesn't exist.
|
|
214
|
-
///
|
|
215
|
-
/// # Examples
|
|
216
|
-
///
|
|
217
|
-
/// ```ignore
|
|
218
|
-
/// use spikard_core::di::DependencyGraph;
|
|
219
|
-
///
|
|
220
|
-
/// let mut graph = DependencyGraph::new();
|
|
221
|
-
/// graph.add_dependency("a", vec![]).unwrap();
|
|
222
|
-
/// graph.add_dependency("b", vec![]).unwrap();
|
|
223
|
-
/// graph.add_dependency("c", vec!["a".to_string(), "b".to_string()]).unwrap();
|
|
224
|
-
///
|
|
225
|
-
/// let batches = graph.calculate_batches(&[
|
|
226
|
-
/// "a".to_string(),
|
|
227
|
-
/// "b".to_string(),
|
|
228
|
-
/// "c".to_string(),
|
|
229
|
-
/// ]).unwrap();
|
|
230
|
-
///
|
|
231
|
-
/// // Batch 1: a and b (no dependencies, can run in parallel)
|
|
232
|
-
/// assert_eq!(batches[0].len(), 2);
|
|
233
|
-
/// assert!(batches[0].contains("a"));
|
|
234
|
-
/// assert!(batches[0].contains("b"));
|
|
235
|
-
///
|
|
236
|
-
/// // Batch 2: c (depends on a and b)
|
|
237
|
-
/// assert_eq!(batches[1].len(), 1);
|
|
238
|
-
/// assert!(batches[1].contains("c"));
|
|
239
|
-
/// ```
|
|
240
|
-
pub fn calculate_batches(&self, keys: &[String]) -> Result<Vec<HashSet<String>>, DependencyError> {
|
|
241
|
-
// Build subgraph with only the requested keys and their transitive dependencies
|
|
242
|
-
let mut subgraph = HashMap::new();
|
|
243
|
-
let mut to_visit: VecDeque<String> = keys.iter().cloned().collect();
|
|
244
|
-
let mut visited = HashSet::new();
|
|
245
|
-
|
|
246
|
-
while let Some(key) = to_visit.pop_front() {
|
|
247
|
-
if visited.contains(&key) {
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
visited.insert(key.clone());
|
|
251
|
-
|
|
252
|
-
if let Some(deps) = self.graph.get(&key) {
|
|
253
|
-
subgraph.insert(key.clone(), deps.clone());
|
|
254
|
-
for dep in deps {
|
|
255
|
-
to_visit.push_back(dep.clone());
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
// Key not in graph - treat as having no dependencies
|
|
259
|
-
subgraph.insert(key.clone(), vec![]);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Calculate in-degree for each node in the subgraph
|
|
264
|
-
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
265
|
-
for key in subgraph.keys() {
|
|
266
|
-
in_degree.entry(key.clone()).or_insert(0);
|
|
267
|
-
}
|
|
268
|
-
for deps in subgraph.values() {
|
|
269
|
-
for dep in deps {
|
|
270
|
-
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Kahn's algorithm for topological sort with batching
|
|
275
|
-
let mut batches = Vec::new();
|
|
276
|
-
let mut queue: VecDeque<String> = in_degree
|
|
277
|
-
.iter()
|
|
278
|
-
.filter(|&(_, °ree)| degree == 0)
|
|
279
|
-
.map(|(key, _)| key.clone())
|
|
280
|
-
.collect();
|
|
281
|
-
|
|
282
|
-
let mut processed = 0;
|
|
283
|
-
let total = subgraph.len();
|
|
284
|
-
|
|
285
|
-
while !queue.is_empty() {
|
|
286
|
-
// All items in the queue can be processed in parallel (same batch)
|
|
287
|
-
let mut batch = HashSet::new();
|
|
288
|
-
|
|
289
|
-
// Process all nodes with in-degree 0
|
|
290
|
-
let batch_size = queue.len();
|
|
291
|
-
for _ in 0..batch_size {
|
|
292
|
-
if let Some(node) = queue.pop_front() {
|
|
293
|
-
batch.insert(node.clone());
|
|
294
|
-
processed += 1;
|
|
295
|
-
|
|
296
|
-
// Reduce in-degree for dependents
|
|
297
|
-
if let Some(deps) = subgraph.get(&node) {
|
|
298
|
-
for dep in deps {
|
|
299
|
-
if let Some(degree) = in_degree.get_mut(dep) {
|
|
300
|
-
*degree -= 1;
|
|
301
|
-
if *degree == 0 {
|
|
302
|
-
queue.push_back(dep.clone());
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if !batch.is_empty() {
|
|
311
|
-
batches.push(batch);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Check if we processed all nodes (if not, there's a cycle)
|
|
316
|
-
if processed < total {
|
|
317
|
-
// Find a cycle by tracing from any unprocessed node
|
|
318
|
-
let unprocessed: Vec<String> = subgraph
|
|
319
|
-
.keys()
|
|
320
|
-
.filter(|k| in_degree.get(*k).is_some_and(|&d| d > 0))
|
|
321
|
-
.cloned()
|
|
322
|
-
.collect();
|
|
323
|
-
|
|
324
|
-
if let Some(start) = unprocessed.first() {
|
|
325
|
-
// Trace the cycle path
|
|
326
|
-
let mut cycle = vec![start.clone()];
|
|
327
|
-
let mut current = start;
|
|
328
|
-
let mut visited_in_path = HashSet::new();
|
|
329
|
-
visited_in_path.insert(start.clone());
|
|
330
|
-
|
|
331
|
-
// Follow dependencies until we find the cycle
|
|
332
|
-
while let Some(deps) = subgraph.get(current) {
|
|
333
|
-
if let Some(next) = deps.iter().find(|d| unprocessed.contains(d)) {
|
|
334
|
-
if visited_in_path.contains(next) {
|
|
335
|
-
// Found the cycle - add the closing node
|
|
336
|
-
cycle.push(next.clone());
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
cycle.push(next.clone());
|
|
340
|
-
visited_in_path.insert(next.clone());
|
|
341
|
-
current = next;
|
|
342
|
-
} else {
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Normalize the cycle to start with the lexicographically smallest element
|
|
348
|
-
// This makes cycle detection deterministic
|
|
349
|
-
// The cycle includes the closing element (first element repeated at end)
|
|
350
|
-
// e.g., [A, B, A] or [B, A, B]
|
|
351
|
-
if cycle.len() > 1 {
|
|
352
|
-
// Find the index of the smallest element (ignoring the last closing element)
|
|
353
|
-
if let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s) {
|
|
354
|
-
cycle.rotate_left(min_idx);
|
|
355
|
-
// After rotation, update the closing element to match the new first element
|
|
356
|
-
if let Some(first) = cycle.first().cloned()
|
|
357
|
-
&& let Some(last) = cycle.last_mut()
|
|
358
|
-
{
|
|
359
|
-
*last = first;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return Err(DependencyError::CircularDependency { cycle });
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Fallback if we can't trace the cycle
|
|
368
|
-
return Err(DependencyError::CircularDependency { cycle: unprocessed });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Reverse the batches because we built them in reverse order
|
|
372
|
-
// (dependencies come before dependents in our graph structure)
|
|
373
|
-
batches.reverse();
|
|
374
|
-
|
|
375
|
-
Ok(batches)
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
#[cfg(test)]
|
|
380
|
-
mod tests {
|
|
381
|
-
use super::*;
|
|
382
|
-
|
|
383
|
-
#[test]
|
|
384
|
-
fn test_new() {
|
|
385
|
-
let graph = DependencyGraph::new();
|
|
386
|
-
assert_eq!(graph.graph.len(), 0);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
#[test]
|
|
390
|
-
fn test_add_dependency_simple() {
|
|
391
|
-
let mut graph = DependencyGraph::new();
|
|
392
|
-
assert!(graph.add_dependency("a", vec![]).is_ok());
|
|
393
|
-
assert!(graph.graph.contains_key("a"));
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
#[test]
|
|
397
|
-
fn test_add_dependency_duplicate() {
|
|
398
|
-
let mut graph = DependencyGraph::new();
|
|
399
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
400
|
-
let result = graph.add_dependency("a", vec![]);
|
|
401
|
-
assert!(matches!(result, Err(DependencyError::DuplicateKey { .. })));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
#[test]
|
|
405
|
-
fn test_has_cycle_simple() {
|
|
406
|
-
let mut graph = DependencyGraph::new();
|
|
407
|
-
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
408
|
-
|
|
409
|
-
// a -> b -> a would be a cycle
|
|
410
|
-
assert!(graph.has_cycle_with("b", &["a".to_string()]));
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
#[test]
|
|
414
|
-
fn test_has_cycle_complex() {
|
|
415
|
-
let mut graph = DependencyGraph::new();
|
|
416
|
-
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
417
|
-
graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
418
|
-
|
|
419
|
-
// c -> a would create cycle: a -> b -> c -> a
|
|
420
|
-
assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
#[test]
|
|
424
|
-
fn test_has_cycle_self_loop() {
|
|
425
|
-
let graph = DependencyGraph::new();
|
|
426
|
-
|
|
427
|
-
// Self-loop: a -> a
|
|
428
|
-
assert!(graph.has_cycle_with("a", &["a".to_string()]));
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
#[test]
|
|
432
|
-
fn test_no_cycle() {
|
|
433
|
-
let mut graph = DependencyGraph::new();
|
|
434
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
435
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
436
|
-
|
|
437
|
-
// c -> a is fine (no cycle)
|
|
438
|
-
assert!(!graph.has_cycle_with("c", &["a".to_string()]));
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
#[test]
|
|
442
|
-
fn test_calculate_batches_simple() {
|
|
443
|
-
let mut graph = DependencyGraph::new();
|
|
444
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
445
|
-
|
|
446
|
-
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
447
|
-
assert_eq!(batches.len(), 1);
|
|
448
|
-
assert!(batches[0].contains("a"));
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
#[test]
|
|
452
|
-
fn test_calculate_batches_linear() {
|
|
453
|
-
let mut graph = DependencyGraph::new();
|
|
454
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
455
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
456
|
-
graph.add_dependency("c", vec!["b".to_string()]).unwrap();
|
|
457
|
-
|
|
458
|
-
let batches = graph
|
|
459
|
-
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
460
|
-
.unwrap();
|
|
461
|
-
|
|
462
|
-
// Should be 3 batches in order: a, b, c
|
|
463
|
-
assert_eq!(batches.len(), 3);
|
|
464
|
-
assert!(batches[0].contains("a"));
|
|
465
|
-
assert!(batches[1].contains("b"));
|
|
466
|
-
assert!(batches[2].contains("c"));
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
#[test]
|
|
470
|
-
fn test_calculate_batches_parallel() {
|
|
471
|
-
let mut graph = DependencyGraph::new();
|
|
472
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
473
|
-
graph.add_dependency("b", vec![]).unwrap();
|
|
474
|
-
graph
|
|
475
|
-
.add_dependency("c", vec!["a".to_string(), "b".to_string()])
|
|
476
|
-
.unwrap();
|
|
477
|
-
|
|
478
|
-
let batches = graph
|
|
479
|
-
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
480
|
-
.unwrap();
|
|
481
|
-
|
|
482
|
-
// Batch 1: a and b (parallel)
|
|
483
|
-
// Batch 2: c
|
|
484
|
-
assert_eq!(batches.len(), 2);
|
|
485
|
-
assert_eq!(batches[0].len(), 2);
|
|
486
|
-
assert!(batches[0].contains("a"));
|
|
487
|
-
assert!(batches[0].contains("b"));
|
|
488
|
-
assert!(batches[1].contains("c"));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
#[test]
|
|
492
|
-
fn test_calculate_batches_nested() {
|
|
493
|
-
let mut graph = DependencyGraph::new();
|
|
494
|
-
graph.add_dependency("config", vec![]).unwrap();
|
|
495
|
-
graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
496
|
-
graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
497
|
-
graph
|
|
498
|
-
.add_dependency("service", vec!["database".to_string(), "cache".to_string()])
|
|
499
|
-
.unwrap();
|
|
500
|
-
|
|
501
|
-
let batches = graph
|
|
502
|
-
.calculate_batches(&[
|
|
503
|
-
"config".to_string(),
|
|
504
|
-
"database".to_string(),
|
|
505
|
-
"cache".to_string(),
|
|
506
|
-
"service".to_string(),
|
|
507
|
-
])
|
|
508
|
-
.unwrap();
|
|
509
|
-
|
|
510
|
-
// Batch 1: config
|
|
511
|
-
// Batch 2: database, cache (parallel)
|
|
512
|
-
// Batch 3: service
|
|
513
|
-
assert_eq!(batches.len(), 3);
|
|
514
|
-
assert!(batches[0].contains("config"));
|
|
515
|
-
assert_eq!(batches[1].len(), 2);
|
|
516
|
-
assert!(batches[1].contains("database"));
|
|
517
|
-
assert!(batches[1].contains("cache"));
|
|
518
|
-
assert!(batches[2].contains("service"));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
#[test]
|
|
522
|
-
fn test_calculate_batches_partial() {
|
|
523
|
-
let mut graph = DependencyGraph::new();
|
|
524
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
525
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
526
|
-
graph.add_dependency("c", vec!["a".to_string()]).unwrap();
|
|
527
|
-
|
|
528
|
-
// Only request b (should also include its dependency a)
|
|
529
|
-
let batches = graph.calculate_batches(&["b".to_string()]).unwrap();
|
|
530
|
-
|
|
531
|
-
assert_eq!(batches.len(), 2);
|
|
532
|
-
assert!(batches[0].contains("a"));
|
|
533
|
-
assert!(batches[1].contains("b"));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
#[test]
|
|
537
|
-
fn test_calculate_batches_missing_dependency() {
|
|
538
|
-
let mut graph = DependencyGraph::new();
|
|
539
|
-
graph.add_dependency("a", vec!["missing".to_string()]).unwrap();
|
|
540
|
-
|
|
541
|
-
// Should handle missing dependencies gracefully
|
|
542
|
-
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
543
|
-
assert!(!batches.is_empty());
|
|
544
|
-
}
|
|
545
|
-
}
|
|
1
|
+
//! Dependency graph with topological sorting and cycle detection
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides the `DependencyGraph` type which manages the dependency
|
|
4
|
+
//! relationships between registered dependencies, detects cycles, and calculates
|
|
5
|
+
//! the optimal batched resolution order.
|
|
6
|
+
|
|
7
|
+
use super::error::DependencyError;
|
|
8
|
+
use std::collections::{HashMap, HashSet, VecDeque};
|
|
9
|
+
|
|
10
|
+
/// Dependency graph for managing dependency relationships
|
|
11
|
+
///
|
|
12
|
+
/// The graph tracks which dependencies depend on which other dependencies,
|
|
13
|
+
/// and provides algorithms for:
|
|
14
|
+
/// - Cycle detection (preventing circular dependencies)
|
|
15
|
+
/// - Topological sorting (determining resolution order)
|
|
16
|
+
/// - Batch calculation (enabling parallel resolution)
|
|
17
|
+
///
|
|
18
|
+
/// # Examples
|
|
19
|
+
///
|
|
20
|
+
/// ```ignore
|
|
21
|
+
/// use spikard_core::di::DependencyGraph;
|
|
22
|
+
///
|
|
23
|
+
/// let mut graph = DependencyGraph::new();
|
|
24
|
+
///
|
|
25
|
+
/// // Add dependencies
|
|
26
|
+
/// graph.add_dependency("config", vec![]).unwrap();
|
|
27
|
+
/// graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
28
|
+
/// graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
29
|
+
/// graph.add_dependency("service", vec!["database".to_string(), "cache".to_string()]).unwrap();
|
|
30
|
+
///
|
|
31
|
+
/// // Calculate batches for parallel resolution
|
|
32
|
+
/// let batches = graph.calculate_batches(&[
|
|
33
|
+
/// "config".to_string(),
|
|
34
|
+
/// "database".to_string(),
|
|
35
|
+
/// "cache".to_string(),
|
|
36
|
+
/// "service".to_string(),
|
|
37
|
+
/// ]).unwrap();
|
|
38
|
+
///
|
|
39
|
+
/// // Batch 1: config (no dependencies)
|
|
40
|
+
/// // Batch 2: database, cache (both depend only on config, can run in parallel)
|
|
41
|
+
/// // Batch 3: service (depends on database and cache)
|
|
42
|
+
/// assert_eq!(batches.len(), 3);
|
|
43
|
+
/// ```
|
|
44
|
+
#[derive(Debug, Clone, Default)]
|
|
45
|
+
pub struct DependencyGraph {
|
|
46
|
+
/// Adjacency list: key -> list of dependencies it depends on
|
|
47
|
+
graph: HashMap<String, Vec<String>>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl DependencyGraph {
|
|
51
|
+
/// Create a new empty dependency graph
|
|
52
|
+
///
|
|
53
|
+
/// # Examples
|
|
54
|
+
///
|
|
55
|
+
/// ```ignore
|
|
56
|
+
/// use spikard_core::di::DependencyGraph;
|
|
57
|
+
///
|
|
58
|
+
/// let graph = DependencyGraph::new();
|
|
59
|
+
/// ```
|
|
60
|
+
#[must_use]
|
|
61
|
+
pub fn new() -> Self {
|
|
62
|
+
Self { graph: HashMap::new() }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Add a dependency and its dependencies to the graph
|
|
66
|
+
///
|
|
67
|
+
/// This will check for cycles before adding. If adding this dependency
|
|
68
|
+
/// would create a cycle, it returns an error.
|
|
69
|
+
///
|
|
70
|
+
/// # Arguments
|
|
71
|
+
///
|
|
72
|
+
/// * `key` - The unique key for this dependency
|
|
73
|
+
/// * `depends_on` - List of dependency keys that this depends on
|
|
74
|
+
///
|
|
75
|
+
/// # Errors
|
|
76
|
+
///
|
|
77
|
+
/// Returns `DependencyError::CircularDependency` if adding this dependency
|
|
78
|
+
/// would create a cycle in the graph.
|
|
79
|
+
///
|
|
80
|
+
/// Returns `DependencyError::DuplicateKey` if a dependency with this key
|
|
81
|
+
/// already exists.
|
|
82
|
+
///
|
|
83
|
+
/// # Examples
|
|
84
|
+
///
|
|
85
|
+
/// ```ignore
|
|
86
|
+
/// use spikard_core::di::DependencyGraph;
|
|
87
|
+
///
|
|
88
|
+
/// let mut graph = DependencyGraph::new();
|
|
89
|
+
///
|
|
90
|
+
/// // Simple dependency chain
|
|
91
|
+
/// graph.add_dependency("a", vec![]).unwrap();
|
|
92
|
+
/// graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
93
|
+
///
|
|
94
|
+
/// // This would create a cycle: a -> b -> a
|
|
95
|
+
/// let result = graph.add_dependency("a", vec!["b".to_string()]);
|
|
96
|
+
/// assert!(result.is_err());
|
|
97
|
+
/// ```
|
|
98
|
+
pub fn add_dependency(&mut self, key: &str, depends_on: Vec<String>) -> Result<(), DependencyError> {
|
|
99
|
+
// Check for duplicate
|
|
100
|
+
if self.graph.contains_key(key) {
|
|
101
|
+
return Err(DependencyError::DuplicateKey { key: key.to_string() });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Don't check for cycles here - allow registration and detect at resolution time
|
|
105
|
+
// This allows the server to start and return proper HTTP error responses
|
|
106
|
+
self.graph.insert(key.to_string(), depends_on);
|
|
107
|
+
Ok(())
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Check if adding a new dependency would create a cycle
|
|
111
|
+
///
|
|
112
|
+
/// Uses depth-first search to detect cycles in the graph if the new
|
|
113
|
+
/// dependency were added.
|
|
114
|
+
///
|
|
115
|
+
/// # Arguments
|
|
116
|
+
///
|
|
117
|
+
/// * `new_key` - The key of the dependency to potentially add
|
|
118
|
+
/// * `new_deps` - The dependencies that the new dependency would depend on
|
|
119
|
+
///
|
|
120
|
+
/// # Returns
|
|
121
|
+
///
|
|
122
|
+
/// `true` if adding this dependency would create a cycle, `false` otherwise.
|
|
123
|
+
///
|
|
124
|
+
/// # Examples
|
|
125
|
+
///
|
|
126
|
+
/// ```ignore
|
|
127
|
+
/// use spikard_core::di::DependencyGraph;
|
|
128
|
+
///
|
|
129
|
+
/// let mut graph = DependencyGraph::new();
|
|
130
|
+
/// graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
131
|
+
/// graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
132
|
+
///
|
|
133
|
+
/// // Adding c -> a would create a cycle
|
|
134
|
+
/// assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
135
|
+
///
|
|
136
|
+
/// // Adding c -> [] would not
|
|
137
|
+
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
138
|
+
/// ```
|
|
139
|
+
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
140
|
+
// Build temporary graph with the new dependency
|
|
141
|
+
let mut temp_graph = self.graph.clone();
|
|
142
|
+
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|
|
143
|
+
|
|
144
|
+
// DFS cycle detection
|
|
145
|
+
let mut visited = HashSet::new();
|
|
146
|
+
let mut rec_stack = HashSet::new();
|
|
147
|
+
|
|
148
|
+
for key in temp_graph.keys() {
|
|
149
|
+
if !visited.contains(key) && Self::has_cycle_dfs(key, &temp_graph, &mut visited, &mut rec_stack) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
false
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Depth-first search for cycle detection
|
|
158
|
+
///
|
|
159
|
+
/// # Arguments
|
|
160
|
+
///
|
|
161
|
+
/// * `node` - Current node being visited
|
|
162
|
+
/// * `graph` - The graph to search
|
|
163
|
+
/// * `visited` - Set of all visited nodes
|
|
164
|
+
/// * `rec_stack` - Set of nodes in the current recursion stack
|
|
165
|
+
///
|
|
166
|
+
/// # Returns
|
|
167
|
+
///
|
|
168
|
+
/// `true` if a cycle is detected, `false` otherwise.
|
|
169
|
+
fn has_cycle_dfs(
|
|
170
|
+
node: &str,
|
|
171
|
+
graph: &HashMap<String, Vec<String>>,
|
|
172
|
+
visited: &mut HashSet<String>,
|
|
173
|
+
rec_stack: &mut HashSet<String>,
|
|
174
|
+
) -> bool {
|
|
175
|
+
visited.insert(node.to_string());
|
|
176
|
+
rec_stack.insert(node.to_string());
|
|
177
|
+
|
|
178
|
+
if let Some(deps) = graph.get(node) {
|
|
179
|
+
for dep in deps {
|
|
180
|
+
if !visited.contains(dep) {
|
|
181
|
+
if Self::has_cycle_dfs(dep, graph, visited, rec_stack) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
} else if rec_stack.contains(dep) {
|
|
185
|
+
// Found a back edge (cycle)
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
rec_stack.remove(node);
|
|
192
|
+
false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Calculate batches of dependencies that can be resolved in parallel
|
|
196
|
+
///
|
|
197
|
+
/// Uses topological sorting with Kahn's algorithm to determine the order
|
|
198
|
+
/// in which dependencies should be resolved. Dependencies with no unresolved
|
|
199
|
+
/// dependencies can be resolved in parallel (same batch).
|
|
200
|
+
///
|
|
201
|
+
/// # Arguments
|
|
202
|
+
///
|
|
203
|
+
/// * `keys` - The dependency keys to resolve
|
|
204
|
+
///
|
|
205
|
+
/// # Returns
|
|
206
|
+
///
|
|
207
|
+
/// A vector of batches, where each batch is a set of dependency keys that
|
|
208
|
+
/// can be resolved in parallel. Batches must be executed sequentially.
|
|
209
|
+
///
|
|
210
|
+
/// # Errors
|
|
211
|
+
///
|
|
212
|
+
/// Returns `DependencyError::CircularDependency` if the graph contains a cycle.
|
|
213
|
+
/// Returns `DependencyError::NotFound` if a requested dependency doesn't exist.
|
|
214
|
+
///
|
|
215
|
+
/// # Examples
|
|
216
|
+
///
|
|
217
|
+
/// ```ignore
|
|
218
|
+
/// use spikard_core::di::DependencyGraph;
|
|
219
|
+
///
|
|
220
|
+
/// let mut graph = DependencyGraph::new();
|
|
221
|
+
/// graph.add_dependency("a", vec![]).unwrap();
|
|
222
|
+
/// graph.add_dependency("b", vec![]).unwrap();
|
|
223
|
+
/// graph.add_dependency("c", vec!["a".to_string(), "b".to_string()]).unwrap();
|
|
224
|
+
///
|
|
225
|
+
/// let batches = graph.calculate_batches(&[
|
|
226
|
+
/// "a".to_string(),
|
|
227
|
+
/// "b".to_string(),
|
|
228
|
+
/// "c".to_string(),
|
|
229
|
+
/// ]).unwrap();
|
|
230
|
+
///
|
|
231
|
+
/// // Batch 1: a and b (no dependencies, can run in parallel)
|
|
232
|
+
/// assert_eq!(batches[0].len(), 2);
|
|
233
|
+
/// assert!(batches[0].contains("a"));
|
|
234
|
+
/// assert!(batches[0].contains("b"));
|
|
235
|
+
///
|
|
236
|
+
/// // Batch 2: c (depends on a and b)
|
|
237
|
+
/// assert_eq!(batches[1].len(), 1);
|
|
238
|
+
/// assert!(batches[1].contains("c"));
|
|
239
|
+
/// ```
|
|
240
|
+
pub fn calculate_batches(&self, keys: &[String]) -> Result<Vec<HashSet<String>>, DependencyError> {
|
|
241
|
+
// Build subgraph with only the requested keys and their transitive dependencies
|
|
242
|
+
let mut subgraph = HashMap::new();
|
|
243
|
+
let mut to_visit: VecDeque<String> = keys.iter().cloned().collect();
|
|
244
|
+
let mut visited = HashSet::new();
|
|
245
|
+
|
|
246
|
+
while let Some(key) = to_visit.pop_front() {
|
|
247
|
+
if visited.contains(&key) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
visited.insert(key.clone());
|
|
251
|
+
|
|
252
|
+
if let Some(deps) = self.graph.get(&key) {
|
|
253
|
+
subgraph.insert(key.clone(), deps.clone());
|
|
254
|
+
for dep in deps {
|
|
255
|
+
to_visit.push_back(dep.clone());
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// Key not in graph - treat as having no dependencies
|
|
259
|
+
subgraph.insert(key.clone(), vec![]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Calculate in-degree for each node in the subgraph
|
|
264
|
+
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
265
|
+
for key in subgraph.keys() {
|
|
266
|
+
in_degree.entry(key.clone()).or_insert(0);
|
|
267
|
+
}
|
|
268
|
+
for deps in subgraph.values() {
|
|
269
|
+
for dep in deps {
|
|
270
|
+
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Kahn's algorithm for topological sort with batching
|
|
275
|
+
let mut batches = Vec::new();
|
|
276
|
+
let mut queue: VecDeque<String> = in_degree
|
|
277
|
+
.iter()
|
|
278
|
+
.filter(|&(_, °ree)| degree == 0)
|
|
279
|
+
.map(|(key, _)| key.clone())
|
|
280
|
+
.collect();
|
|
281
|
+
|
|
282
|
+
let mut processed = 0;
|
|
283
|
+
let total = subgraph.len();
|
|
284
|
+
|
|
285
|
+
while !queue.is_empty() {
|
|
286
|
+
// All items in the queue can be processed in parallel (same batch)
|
|
287
|
+
let mut batch = HashSet::new();
|
|
288
|
+
|
|
289
|
+
// Process all nodes with in-degree 0
|
|
290
|
+
let batch_size = queue.len();
|
|
291
|
+
for _ in 0..batch_size {
|
|
292
|
+
if let Some(node) = queue.pop_front() {
|
|
293
|
+
batch.insert(node.clone());
|
|
294
|
+
processed += 1;
|
|
295
|
+
|
|
296
|
+
// Reduce in-degree for dependents
|
|
297
|
+
if let Some(deps) = subgraph.get(&node) {
|
|
298
|
+
for dep in deps {
|
|
299
|
+
if let Some(degree) = in_degree.get_mut(dep) {
|
|
300
|
+
*degree -= 1;
|
|
301
|
+
if *degree == 0 {
|
|
302
|
+
queue.push_back(dep.clone());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if !batch.is_empty() {
|
|
311
|
+
batches.push(batch);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if we processed all nodes (if not, there's a cycle)
|
|
316
|
+
if processed < total {
|
|
317
|
+
// Find a cycle by tracing from any unprocessed node
|
|
318
|
+
let unprocessed: Vec<String> = subgraph
|
|
319
|
+
.keys()
|
|
320
|
+
.filter(|k| in_degree.get(*k).is_some_and(|&d| d > 0))
|
|
321
|
+
.cloned()
|
|
322
|
+
.collect();
|
|
323
|
+
|
|
324
|
+
if let Some(start) = unprocessed.first() {
|
|
325
|
+
// Trace the cycle path
|
|
326
|
+
let mut cycle = vec![start.clone()];
|
|
327
|
+
let mut current = start;
|
|
328
|
+
let mut visited_in_path = HashSet::new();
|
|
329
|
+
visited_in_path.insert(start.clone());
|
|
330
|
+
|
|
331
|
+
// Follow dependencies until we find the cycle
|
|
332
|
+
while let Some(deps) = subgraph.get(current) {
|
|
333
|
+
if let Some(next) = deps.iter().find(|d| unprocessed.contains(d)) {
|
|
334
|
+
if visited_in_path.contains(next) {
|
|
335
|
+
// Found the cycle - add the closing node
|
|
336
|
+
cycle.push(next.clone());
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
cycle.push(next.clone());
|
|
340
|
+
visited_in_path.insert(next.clone());
|
|
341
|
+
current = next;
|
|
342
|
+
} else {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Normalize the cycle to start with the lexicographically smallest element
|
|
348
|
+
// This makes cycle detection deterministic
|
|
349
|
+
// The cycle includes the closing element (first element repeated at end)
|
|
350
|
+
// e.g., [A, B, A] or [B, A, B]
|
|
351
|
+
if cycle.len() > 1 {
|
|
352
|
+
// Find the index of the smallest element (ignoring the last closing element)
|
|
353
|
+
if let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s) {
|
|
354
|
+
cycle.rotate_left(min_idx);
|
|
355
|
+
// After rotation, update the closing element to match the new first element
|
|
356
|
+
if let Some(first) = cycle.first().cloned()
|
|
357
|
+
&& let Some(last) = cycle.last_mut()
|
|
358
|
+
{
|
|
359
|
+
*last = first;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return Err(DependencyError::CircularDependency { cycle });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Fallback if we can't trace the cycle
|
|
368
|
+
return Err(DependencyError::CircularDependency { cycle: unprocessed });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Reverse the batches because we built them in reverse order
|
|
372
|
+
// (dependencies come before dependents in our graph structure)
|
|
373
|
+
batches.reverse();
|
|
374
|
+
|
|
375
|
+
Ok(batches)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
#[cfg(test)]
|
|
380
|
+
mod tests {
|
|
381
|
+
use super::*;
|
|
382
|
+
|
|
383
|
+
#[test]
|
|
384
|
+
fn test_new() {
|
|
385
|
+
let graph = DependencyGraph::new();
|
|
386
|
+
assert_eq!(graph.graph.len(), 0);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#[test]
|
|
390
|
+
fn test_add_dependency_simple() {
|
|
391
|
+
let mut graph = DependencyGraph::new();
|
|
392
|
+
assert!(graph.add_dependency("a", vec![]).is_ok());
|
|
393
|
+
assert!(graph.graph.contains_key("a"));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn test_add_dependency_duplicate() {
|
|
398
|
+
let mut graph = DependencyGraph::new();
|
|
399
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
400
|
+
let result = graph.add_dependency("a", vec![]);
|
|
401
|
+
assert!(matches!(result, Err(DependencyError::DuplicateKey { .. })));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[test]
|
|
405
|
+
fn test_has_cycle_simple() {
|
|
406
|
+
let mut graph = DependencyGraph::new();
|
|
407
|
+
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
408
|
+
|
|
409
|
+
// a -> b -> a would be a cycle
|
|
410
|
+
assert!(graph.has_cycle_with("b", &["a".to_string()]));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#[test]
|
|
414
|
+
fn test_has_cycle_complex() {
|
|
415
|
+
let mut graph = DependencyGraph::new();
|
|
416
|
+
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
417
|
+
graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
418
|
+
|
|
419
|
+
// c -> a would create cycle: a -> b -> c -> a
|
|
420
|
+
assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[test]
|
|
424
|
+
fn test_has_cycle_self_loop() {
|
|
425
|
+
let graph = DependencyGraph::new();
|
|
426
|
+
|
|
427
|
+
// Self-loop: a -> a
|
|
428
|
+
assert!(graph.has_cycle_with("a", &["a".to_string()]));
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#[test]
|
|
432
|
+
fn test_no_cycle() {
|
|
433
|
+
let mut graph = DependencyGraph::new();
|
|
434
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
435
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
436
|
+
|
|
437
|
+
// c -> a is fine (no cycle)
|
|
438
|
+
assert!(!graph.has_cycle_with("c", &["a".to_string()]));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
#[test]
|
|
442
|
+
fn test_calculate_batches_simple() {
|
|
443
|
+
let mut graph = DependencyGraph::new();
|
|
444
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
445
|
+
|
|
446
|
+
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
447
|
+
assert_eq!(batches.len(), 1);
|
|
448
|
+
assert!(batches[0].contains("a"));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
#[test]
|
|
452
|
+
fn test_calculate_batches_linear() {
|
|
453
|
+
let mut graph = DependencyGraph::new();
|
|
454
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
455
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
456
|
+
graph.add_dependency("c", vec!["b".to_string()]).unwrap();
|
|
457
|
+
|
|
458
|
+
let batches = graph
|
|
459
|
+
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
460
|
+
.unwrap();
|
|
461
|
+
|
|
462
|
+
// Should be 3 batches in order: a, b, c
|
|
463
|
+
assert_eq!(batches.len(), 3);
|
|
464
|
+
assert!(batches[0].contains("a"));
|
|
465
|
+
assert!(batches[1].contains("b"));
|
|
466
|
+
assert!(batches[2].contains("c"));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#[test]
|
|
470
|
+
fn test_calculate_batches_parallel() {
|
|
471
|
+
let mut graph = DependencyGraph::new();
|
|
472
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
473
|
+
graph.add_dependency("b", vec![]).unwrap();
|
|
474
|
+
graph
|
|
475
|
+
.add_dependency("c", vec!["a".to_string(), "b".to_string()])
|
|
476
|
+
.unwrap();
|
|
477
|
+
|
|
478
|
+
let batches = graph
|
|
479
|
+
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
480
|
+
.unwrap();
|
|
481
|
+
|
|
482
|
+
// Batch 1: a and b (parallel)
|
|
483
|
+
// Batch 2: c
|
|
484
|
+
assert_eq!(batches.len(), 2);
|
|
485
|
+
assert_eq!(batches[0].len(), 2);
|
|
486
|
+
assert!(batches[0].contains("a"));
|
|
487
|
+
assert!(batches[0].contains("b"));
|
|
488
|
+
assert!(batches[1].contains("c"));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
#[test]
|
|
492
|
+
fn test_calculate_batches_nested() {
|
|
493
|
+
let mut graph = DependencyGraph::new();
|
|
494
|
+
graph.add_dependency("config", vec![]).unwrap();
|
|
495
|
+
graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
496
|
+
graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
497
|
+
graph
|
|
498
|
+
.add_dependency("service", vec!["database".to_string(), "cache".to_string()])
|
|
499
|
+
.unwrap();
|
|
500
|
+
|
|
501
|
+
let batches = graph
|
|
502
|
+
.calculate_batches(&[
|
|
503
|
+
"config".to_string(),
|
|
504
|
+
"database".to_string(),
|
|
505
|
+
"cache".to_string(),
|
|
506
|
+
"service".to_string(),
|
|
507
|
+
])
|
|
508
|
+
.unwrap();
|
|
509
|
+
|
|
510
|
+
// Batch 1: config
|
|
511
|
+
// Batch 2: database, cache (parallel)
|
|
512
|
+
// Batch 3: service
|
|
513
|
+
assert_eq!(batches.len(), 3);
|
|
514
|
+
assert!(batches[0].contains("config"));
|
|
515
|
+
assert_eq!(batches[1].len(), 2);
|
|
516
|
+
assert!(batches[1].contains("database"));
|
|
517
|
+
assert!(batches[1].contains("cache"));
|
|
518
|
+
assert!(batches[2].contains("service"));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#[test]
|
|
522
|
+
fn test_calculate_batches_partial() {
|
|
523
|
+
let mut graph = DependencyGraph::new();
|
|
524
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
525
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
526
|
+
graph.add_dependency("c", vec!["a".to_string()]).unwrap();
|
|
527
|
+
|
|
528
|
+
// Only request b (should also include its dependency a)
|
|
529
|
+
let batches = graph.calculate_batches(&["b".to_string()]).unwrap();
|
|
530
|
+
|
|
531
|
+
assert_eq!(batches.len(), 2);
|
|
532
|
+
assert!(batches[0].contains("a"));
|
|
533
|
+
assert!(batches[1].contains("b"));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
#[test]
|
|
537
|
+
fn test_calculate_batches_missing_dependency() {
|
|
538
|
+
let mut graph = DependencyGraph::new();
|
|
539
|
+
graph.add_dependency("a", vec!["missing".to_string()]).unwrap();
|
|
540
|
+
|
|
541
|
+
// Should handle missing dependencies gracefully
|
|
542
|
+
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
543
|
+
assert!(!batches.is_empty());
|
|
544
|
+
}
|
|
545
|
+
}
|