spikard 0.5.0 → 0.6.1
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 +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- 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 +256 -256
- 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 +366 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- 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 +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- 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 +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- 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/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- 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 +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
|
@@ -1,506 +1,506 @@
|
|
|
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
|
-
if self.graph.contains_key(key) {
|
|
100
|
-
return Err(DependencyError::DuplicateKey { key: key.to_string() });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
self.graph.insert(key.to_string(), depends_on);
|
|
104
|
-
Ok(())
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/// Check if adding a new dependency would create a cycle
|
|
108
|
-
///
|
|
109
|
-
/// Uses depth-first search to detect cycles in the graph if the new
|
|
110
|
-
/// dependency were added.
|
|
111
|
-
///
|
|
112
|
-
/// # Arguments
|
|
113
|
-
///
|
|
114
|
-
/// * `new_key` - The key of the dependency to potentially add
|
|
115
|
-
/// * `new_deps` - The dependencies that the new dependency would depend on
|
|
116
|
-
///
|
|
117
|
-
/// # Returns
|
|
118
|
-
///
|
|
119
|
-
/// `true` if adding this dependency would create a cycle, `false` otherwise.
|
|
120
|
-
///
|
|
121
|
-
/// # Examples
|
|
122
|
-
///
|
|
123
|
-
/// ```ignore
|
|
124
|
-
/// use spikard_core::di::DependencyGraph;
|
|
125
|
-
///
|
|
126
|
-
/// let mut graph = DependencyGraph::new();
|
|
127
|
-
/// graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
128
|
-
/// graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
129
|
-
///
|
|
130
|
-
/// // Adding c -> a would create a cycle
|
|
131
|
-
/// assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
132
|
-
///
|
|
133
|
-
/// // Adding c -> [] would not
|
|
134
|
-
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
135
|
-
/// ```
|
|
136
|
-
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
137
|
-
let mut temp_graph = self.graph.clone();
|
|
138
|
-
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|
|
139
|
-
|
|
140
|
-
let mut visited = HashSet::new();
|
|
141
|
-
let mut rec_stack = HashSet::new();
|
|
142
|
-
|
|
143
|
-
for key in temp_graph.keys() {
|
|
144
|
-
if !visited.contains(key) && Self::has_cycle_dfs(key, &temp_graph, &mut visited, &mut rec_stack) {
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
false
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/// Depth-first search for cycle detection
|
|
153
|
-
///
|
|
154
|
-
/// # Arguments
|
|
155
|
-
///
|
|
156
|
-
/// * `node` - Current node being visited
|
|
157
|
-
/// * `graph` - The graph to search
|
|
158
|
-
/// * `visited` - Set of all visited nodes
|
|
159
|
-
/// * `rec_stack` - Set of nodes in the current recursion stack
|
|
160
|
-
///
|
|
161
|
-
/// # Returns
|
|
162
|
-
///
|
|
163
|
-
/// `true` if a cycle is detected, `false` otherwise.
|
|
164
|
-
fn has_cycle_dfs(
|
|
165
|
-
node: &str,
|
|
166
|
-
graph: &HashMap<String, Vec<String>>,
|
|
167
|
-
visited: &mut HashSet<String>,
|
|
168
|
-
rec_stack: &mut HashSet<String>,
|
|
169
|
-
) -> bool {
|
|
170
|
-
visited.insert(node.to_string());
|
|
171
|
-
rec_stack.insert(node.to_string());
|
|
172
|
-
|
|
173
|
-
if let Some(deps) = graph.get(node) {
|
|
174
|
-
for dep in deps {
|
|
175
|
-
if !visited.contains(dep) {
|
|
176
|
-
if Self::has_cycle_dfs(dep, graph, visited, rec_stack) {
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
} else if rec_stack.contains(dep) {
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
rec_stack.remove(node);
|
|
186
|
-
false
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/// Calculate batches of dependencies that can be resolved in parallel
|
|
190
|
-
///
|
|
191
|
-
/// Uses topological sorting with Kahn's algorithm to determine the order
|
|
192
|
-
/// in which dependencies should be resolved. Dependencies with no unresolved
|
|
193
|
-
/// dependencies can be resolved in parallel (same batch).
|
|
194
|
-
///
|
|
195
|
-
/// # Arguments
|
|
196
|
-
///
|
|
197
|
-
/// * `keys` - The dependency keys to resolve
|
|
198
|
-
///
|
|
199
|
-
/// # Returns
|
|
200
|
-
///
|
|
201
|
-
/// A vector of batches, where each batch is a set of dependency keys that
|
|
202
|
-
/// can be resolved in parallel. Batches must be executed sequentially.
|
|
203
|
-
///
|
|
204
|
-
/// # Errors
|
|
205
|
-
///
|
|
206
|
-
/// Returns `DependencyError::CircularDependency` if the graph contains a cycle.
|
|
207
|
-
/// Returns `DependencyError::NotFound` if a requested dependency doesn't exist.
|
|
208
|
-
///
|
|
209
|
-
/// # Examples
|
|
210
|
-
///
|
|
211
|
-
/// ```ignore
|
|
212
|
-
/// use spikard_core::di::DependencyGraph;
|
|
213
|
-
///
|
|
214
|
-
/// let mut graph = DependencyGraph::new();
|
|
215
|
-
/// graph.add_dependency("a", vec![]).unwrap();
|
|
216
|
-
/// graph.add_dependency("b", vec![]).unwrap();
|
|
217
|
-
/// graph.add_dependency("c", vec!["a".to_string(), "b".to_string()]).unwrap();
|
|
218
|
-
///
|
|
219
|
-
/// let batches = graph.calculate_batches(&[
|
|
220
|
-
/// "a".to_string(),
|
|
221
|
-
/// "b".to_string(),
|
|
222
|
-
/// "c".to_string(),
|
|
223
|
-
/// ]).unwrap();
|
|
224
|
-
///
|
|
225
|
-
/// // Batch 1: a and b (no dependencies, can run in parallel)
|
|
226
|
-
/// assert_eq!(batches[0].len(), 2);
|
|
227
|
-
/// assert!(batches[0].contains("a"));
|
|
228
|
-
/// assert!(batches[0].contains("b"));
|
|
229
|
-
///
|
|
230
|
-
/// // Batch 2: c (depends on a and b)
|
|
231
|
-
/// assert_eq!(batches[1].len(), 1);
|
|
232
|
-
/// assert!(batches[1].contains("c"));
|
|
233
|
-
/// ```
|
|
234
|
-
pub fn calculate_batches(&self, keys: &[String]) -> Result<Vec<HashSet<String>>, DependencyError> {
|
|
235
|
-
let mut subgraph = HashMap::new();
|
|
236
|
-
let mut to_visit: VecDeque<String> = keys.iter().cloned().collect();
|
|
237
|
-
let mut visited = HashSet::new();
|
|
238
|
-
|
|
239
|
-
while let Some(key) = to_visit.pop_front() {
|
|
240
|
-
if visited.contains(&key) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
visited.insert(key.clone());
|
|
244
|
-
|
|
245
|
-
if let Some(deps) = self.graph.get(&key) {
|
|
246
|
-
subgraph.insert(key.clone(), deps.clone());
|
|
247
|
-
for dep in deps {
|
|
248
|
-
to_visit.push_back(dep.clone());
|
|
249
|
-
}
|
|
250
|
-
} else {
|
|
251
|
-
subgraph.insert(key.clone(), vec![]);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
256
|
-
for key in subgraph.keys() {
|
|
257
|
-
in_degree.entry(key.clone()).or_insert(0);
|
|
258
|
-
}
|
|
259
|
-
for deps in subgraph.values() {
|
|
260
|
-
for dep in deps {
|
|
261
|
-
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
let mut batches = Vec::new();
|
|
266
|
-
let mut queue: VecDeque<String> = in_degree
|
|
267
|
-
.iter()
|
|
268
|
-
.filter(|&(_, °ree)| degree == 0)
|
|
269
|
-
.map(|(key, _)| key.clone())
|
|
270
|
-
.collect();
|
|
271
|
-
|
|
272
|
-
let mut processed = 0;
|
|
273
|
-
let total = subgraph.len();
|
|
274
|
-
|
|
275
|
-
while !queue.is_empty() {
|
|
276
|
-
let mut batch = HashSet::new();
|
|
277
|
-
|
|
278
|
-
let batch_size = queue.len();
|
|
279
|
-
for _ in 0..batch_size {
|
|
280
|
-
if let Some(node) = queue.pop_front() {
|
|
281
|
-
batch.insert(node.clone());
|
|
282
|
-
processed += 1;
|
|
283
|
-
|
|
284
|
-
if let Some(deps) = subgraph.get(&node) {
|
|
285
|
-
for dep in deps {
|
|
286
|
-
if let Some(degree) = in_degree.get_mut(dep) {
|
|
287
|
-
*degree -= 1;
|
|
288
|
-
if *degree == 0 {
|
|
289
|
-
queue.push_back(dep.clone());
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if !batch.is_empty() {
|
|
298
|
-
batches.push(batch);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if processed < total {
|
|
303
|
-
let unprocessed: Vec<String> = subgraph
|
|
304
|
-
.keys()
|
|
305
|
-
.filter(|k| in_degree.get(*k).is_some_and(|&d| d > 0))
|
|
306
|
-
.cloned()
|
|
307
|
-
.collect();
|
|
308
|
-
|
|
309
|
-
if let Some(start) = unprocessed.first() {
|
|
310
|
-
let mut cycle = vec![start.clone()];
|
|
311
|
-
let mut current = start;
|
|
312
|
-
let mut visited_in_path = HashSet::new();
|
|
313
|
-
visited_in_path.insert(start.clone());
|
|
314
|
-
|
|
315
|
-
while let Some(deps) = subgraph.get(current) {
|
|
316
|
-
if let Some(next) = deps.iter().find(|d| unprocessed.contains(d)) {
|
|
317
|
-
if visited_in_path.contains(next) {
|
|
318
|
-
cycle.push(next.clone());
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
cycle.push(next.clone());
|
|
322
|
-
visited_in_path.insert(next.clone());
|
|
323
|
-
current = next;
|
|
324
|
-
} else {
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if cycle.len() > 1
|
|
330
|
-
&& let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s)
|
|
331
|
-
{
|
|
332
|
-
cycle.rotate_left(min_idx);
|
|
333
|
-
if let Some(first) = cycle.first().cloned()
|
|
334
|
-
&& let Some(last) = cycle.last_mut()
|
|
335
|
-
{
|
|
336
|
-
*last = first;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return Err(DependencyError::CircularDependency { cycle });
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return Err(DependencyError::CircularDependency { cycle: unprocessed });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
batches.reverse();
|
|
347
|
-
|
|
348
|
-
Ok(batches)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
#[cfg(test)]
|
|
353
|
-
mod tests {
|
|
354
|
-
use super::*;
|
|
355
|
-
|
|
356
|
-
#[test]
|
|
357
|
-
fn test_new() {
|
|
358
|
-
let graph = DependencyGraph::new();
|
|
359
|
-
assert_eq!(graph.graph.len(), 0);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
#[test]
|
|
363
|
-
fn test_add_dependency_simple() {
|
|
364
|
-
let mut graph = DependencyGraph::new();
|
|
365
|
-
assert!(graph.add_dependency("a", vec![]).is_ok());
|
|
366
|
-
assert!(graph.graph.contains_key("a"));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
#[test]
|
|
370
|
-
fn test_add_dependency_duplicate() {
|
|
371
|
-
let mut graph = DependencyGraph::new();
|
|
372
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
373
|
-
let result = graph.add_dependency("a", vec![]);
|
|
374
|
-
assert!(matches!(result, Err(DependencyError::DuplicateKey { .. })));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
#[test]
|
|
378
|
-
fn test_has_cycle_simple() {
|
|
379
|
-
let mut graph = DependencyGraph::new();
|
|
380
|
-
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
381
|
-
|
|
382
|
-
assert!(graph.has_cycle_with("b", &["a".to_string()]));
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
#[test]
|
|
386
|
-
fn test_has_cycle_complex() {
|
|
387
|
-
let mut graph = DependencyGraph::new();
|
|
388
|
-
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
389
|
-
graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
390
|
-
|
|
391
|
-
assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
#[test]
|
|
395
|
-
fn test_has_cycle_self_loop() {
|
|
396
|
-
let graph = DependencyGraph::new();
|
|
397
|
-
|
|
398
|
-
assert!(graph.has_cycle_with("a", &["a".to_string()]));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
#[test]
|
|
402
|
-
fn test_no_cycle() {
|
|
403
|
-
let mut graph = DependencyGraph::new();
|
|
404
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
405
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
406
|
-
|
|
407
|
-
assert!(!graph.has_cycle_with("c", &["a".to_string()]));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
#[test]
|
|
411
|
-
fn test_calculate_batches_simple() {
|
|
412
|
-
let mut graph = DependencyGraph::new();
|
|
413
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
414
|
-
|
|
415
|
-
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
416
|
-
assert_eq!(batches.len(), 1);
|
|
417
|
-
assert!(batches[0].contains("a"));
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
#[test]
|
|
421
|
-
fn test_calculate_batches_linear() {
|
|
422
|
-
let mut graph = DependencyGraph::new();
|
|
423
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
424
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
425
|
-
graph.add_dependency("c", vec!["b".to_string()]).unwrap();
|
|
426
|
-
|
|
427
|
-
let batches = graph
|
|
428
|
-
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
429
|
-
.unwrap();
|
|
430
|
-
|
|
431
|
-
assert_eq!(batches.len(), 3);
|
|
432
|
-
assert!(batches[0].contains("a"));
|
|
433
|
-
assert!(batches[1].contains("b"));
|
|
434
|
-
assert!(batches[2].contains("c"));
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
#[test]
|
|
438
|
-
fn test_calculate_batches_parallel() {
|
|
439
|
-
let mut graph = DependencyGraph::new();
|
|
440
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
441
|
-
graph.add_dependency("b", vec![]).unwrap();
|
|
442
|
-
graph
|
|
443
|
-
.add_dependency("c", vec!["a".to_string(), "b".to_string()])
|
|
444
|
-
.unwrap();
|
|
445
|
-
|
|
446
|
-
let batches = graph
|
|
447
|
-
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
448
|
-
.unwrap();
|
|
449
|
-
|
|
450
|
-
assert_eq!(batches.len(), 2);
|
|
451
|
-
assert_eq!(batches[0].len(), 2);
|
|
452
|
-
assert!(batches[0].contains("a"));
|
|
453
|
-
assert!(batches[0].contains("b"));
|
|
454
|
-
assert!(batches[1].contains("c"));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
#[test]
|
|
458
|
-
fn test_calculate_batches_nested() {
|
|
459
|
-
let mut graph = DependencyGraph::new();
|
|
460
|
-
graph.add_dependency("config", vec![]).unwrap();
|
|
461
|
-
graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
462
|
-
graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
463
|
-
graph
|
|
464
|
-
.add_dependency("service", vec!["database".to_string(), "cache".to_string()])
|
|
465
|
-
.unwrap();
|
|
466
|
-
|
|
467
|
-
let batches = graph
|
|
468
|
-
.calculate_batches(&[
|
|
469
|
-
"config".to_string(),
|
|
470
|
-
"database".to_string(),
|
|
471
|
-
"cache".to_string(),
|
|
472
|
-
"service".to_string(),
|
|
473
|
-
])
|
|
474
|
-
.unwrap();
|
|
475
|
-
|
|
476
|
-
assert_eq!(batches.len(), 3);
|
|
477
|
-
assert!(batches[0].contains("config"));
|
|
478
|
-
assert_eq!(batches[1].len(), 2);
|
|
479
|
-
assert!(batches[1].contains("database"));
|
|
480
|
-
assert!(batches[1].contains("cache"));
|
|
481
|
-
assert!(batches[2].contains("service"));
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
#[test]
|
|
485
|
-
fn test_calculate_batches_partial() {
|
|
486
|
-
let mut graph = DependencyGraph::new();
|
|
487
|
-
graph.add_dependency("a", vec![]).unwrap();
|
|
488
|
-
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
489
|
-
graph.add_dependency("c", vec!["a".to_string()]).unwrap();
|
|
490
|
-
|
|
491
|
-
let batches = graph.calculate_batches(&["b".to_string()]).unwrap();
|
|
492
|
-
|
|
493
|
-
assert_eq!(batches.len(), 2);
|
|
494
|
-
assert!(batches[0].contains("a"));
|
|
495
|
-
assert!(batches[1].contains("b"));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
#[test]
|
|
499
|
-
fn test_calculate_batches_missing_dependency() {
|
|
500
|
-
let mut graph = DependencyGraph::new();
|
|
501
|
-
graph.add_dependency("a", vec!["missing".to_string()]).unwrap();
|
|
502
|
-
|
|
503
|
-
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
504
|
-
assert!(!batches.is_empty());
|
|
505
|
-
}
|
|
506
|
-
}
|
|
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
|
+
if self.graph.contains_key(key) {
|
|
100
|
+
return Err(DependencyError::DuplicateKey { key: key.to_string() });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
self.graph.insert(key.to_string(), depends_on);
|
|
104
|
+
Ok(())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Check if adding a new dependency would create a cycle
|
|
108
|
+
///
|
|
109
|
+
/// Uses depth-first search to detect cycles in the graph if the new
|
|
110
|
+
/// dependency were added.
|
|
111
|
+
///
|
|
112
|
+
/// # Arguments
|
|
113
|
+
///
|
|
114
|
+
/// * `new_key` - The key of the dependency to potentially add
|
|
115
|
+
/// * `new_deps` - The dependencies that the new dependency would depend on
|
|
116
|
+
///
|
|
117
|
+
/// # Returns
|
|
118
|
+
///
|
|
119
|
+
/// `true` if adding this dependency would create a cycle, `false` otherwise.
|
|
120
|
+
///
|
|
121
|
+
/// # Examples
|
|
122
|
+
///
|
|
123
|
+
/// ```ignore
|
|
124
|
+
/// use spikard_core::di::DependencyGraph;
|
|
125
|
+
///
|
|
126
|
+
/// let mut graph = DependencyGraph::new();
|
|
127
|
+
/// graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
128
|
+
/// graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
129
|
+
///
|
|
130
|
+
/// // Adding c -> a would create a cycle
|
|
131
|
+
/// assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
132
|
+
///
|
|
133
|
+
/// // Adding c -> [] would not
|
|
134
|
+
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
135
|
+
/// ```
|
|
136
|
+
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
137
|
+
let mut temp_graph = self.graph.clone();
|
|
138
|
+
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|
|
139
|
+
|
|
140
|
+
let mut visited = HashSet::new();
|
|
141
|
+
let mut rec_stack = HashSet::new();
|
|
142
|
+
|
|
143
|
+
for key in temp_graph.keys() {
|
|
144
|
+
if !visited.contains(key) && Self::has_cycle_dfs(key, &temp_graph, &mut visited, &mut rec_stack) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Depth-first search for cycle detection
|
|
153
|
+
///
|
|
154
|
+
/// # Arguments
|
|
155
|
+
///
|
|
156
|
+
/// * `node` - Current node being visited
|
|
157
|
+
/// * `graph` - The graph to search
|
|
158
|
+
/// * `visited` - Set of all visited nodes
|
|
159
|
+
/// * `rec_stack` - Set of nodes in the current recursion stack
|
|
160
|
+
///
|
|
161
|
+
/// # Returns
|
|
162
|
+
///
|
|
163
|
+
/// `true` if a cycle is detected, `false` otherwise.
|
|
164
|
+
fn has_cycle_dfs(
|
|
165
|
+
node: &str,
|
|
166
|
+
graph: &HashMap<String, Vec<String>>,
|
|
167
|
+
visited: &mut HashSet<String>,
|
|
168
|
+
rec_stack: &mut HashSet<String>,
|
|
169
|
+
) -> bool {
|
|
170
|
+
visited.insert(node.to_string());
|
|
171
|
+
rec_stack.insert(node.to_string());
|
|
172
|
+
|
|
173
|
+
if let Some(deps) = graph.get(node) {
|
|
174
|
+
for dep in deps {
|
|
175
|
+
if !visited.contains(dep) {
|
|
176
|
+
if Self::has_cycle_dfs(dep, graph, visited, rec_stack) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
} else if rec_stack.contains(dep) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
rec_stack.remove(node);
|
|
186
|
+
false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Calculate batches of dependencies that can be resolved in parallel
|
|
190
|
+
///
|
|
191
|
+
/// Uses topological sorting with Kahn's algorithm to determine the order
|
|
192
|
+
/// in which dependencies should be resolved. Dependencies with no unresolved
|
|
193
|
+
/// dependencies can be resolved in parallel (same batch).
|
|
194
|
+
///
|
|
195
|
+
/// # Arguments
|
|
196
|
+
///
|
|
197
|
+
/// * `keys` - The dependency keys to resolve
|
|
198
|
+
///
|
|
199
|
+
/// # Returns
|
|
200
|
+
///
|
|
201
|
+
/// A vector of batches, where each batch is a set of dependency keys that
|
|
202
|
+
/// can be resolved in parallel. Batches must be executed sequentially.
|
|
203
|
+
///
|
|
204
|
+
/// # Errors
|
|
205
|
+
///
|
|
206
|
+
/// Returns `DependencyError::CircularDependency` if the graph contains a cycle.
|
|
207
|
+
/// Returns `DependencyError::NotFound` if a requested dependency doesn't exist.
|
|
208
|
+
///
|
|
209
|
+
/// # Examples
|
|
210
|
+
///
|
|
211
|
+
/// ```ignore
|
|
212
|
+
/// use spikard_core::di::DependencyGraph;
|
|
213
|
+
///
|
|
214
|
+
/// let mut graph = DependencyGraph::new();
|
|
215
|
+
/// graph.add_dependency("a", vec![]).unwrap();
|
|
216
|
+
/// graph.add_dependency("b", vec![]).unwrap();
|
|
217
|
+
/// graph.add_dependency("c", vec!["a".to_string(), "b".to_string()]).unwrap();
|
|
218
|
+
///
|
|
219
|
+
/// let batches = graph.calculate_batches(&[
|
|
220
|
+
/// "a".to_string(),
|
|
221
|
+
/// "b".to_string(),
|
|
222
|
+
/// "c".to_string(),
|
|
223
|
+
/// ]).unwrap();
|
|
224
|
+
///
|
|
225
|
+
/// // Batch 1: a and b (no dependencies, can run in parallel)
|
|
226
|
+
/// assert_eq!(batches[0].len(), 2);
|
|
227
|
+
/// assert!(batches[0].contains("a"));
|
|
228
|
+
/// assert!(batches[0].contains("b"));
|
|
229
|
+
///
|
|
230
|
+
/// // Batch 2: c (depends on a and b)
|
|
231
|
+
/// assert_eq!(batches[1].len(), 1);
|
|
232
|
+
/// assert!(batches[1].contains("c"));
|
|
233
|
+
/// ```
|
|
234
|
+
pub fn calculate_batches(&self, keys: &[String]) -> Result<Vec<HashSet<String>>, DependencyError> {
|
|
235
|
+
let mut subgraph = HashMap::new();
|
|
236
|
+
let mut to_visit: VecDeque<String> = keys.iter().cloned().collect();
|
|
237
|
+
let mut visited = HashSet::new();
|
|
238
|
+
|
|
239
|
+
while let Some(key) = to_visit.pop_front() {
|
|
240
|
+
if visited.contains(&key) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
visited.insert(key.clone());
|
|
244
|
+
|
|
245
|
+
if let Some(deps) = self.graph.get(&key) {
|
|
246
|
+
subgraph.insert(key.clone(), deps.clone());
|
|
247
|
+
for dep in deps {
|
|
248
|
+
to_visit.push_back(dep.clone());
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
subgraph.insert(key.clone(), vec![]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
256
|
+
for key in subgraph.keys() {
|
|
257
|
+
in_degree.entry(key.clone()).or_insert(0);
|
|
258
|
+
}
|
|
259
|
+
for deps in subgraph.values() {
|
|
260
|
+
for dep in deps {
|
|
261
|
+
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let mut batches = Vec::new();
|
|
266
|
+
let mut queue: VecDeque<String> = in_degree
|
|
267
|
+
.iter()
|
|
268
|
+
.filter(|&(_, °ree)| degree == 0)
|
|
269
|
+
.map(|(key, _)| key.clone())
|
|
270
|
+
.collect();
|
|
271
|
+
|
|
272
|
+
let mut processed = 0;
|
|
273
|
+
let total = subgraph.len();
|
|
274
|
+
|
|
275
|
+
while !queue.is_empty() {
|
|
276
|
+
let mut batch = HashSet::new();
|
|
277
|
+
|
|
278
|
+
let batch_size = queue.len();
|
|
279
|
+
for _ in 0..batch_size {
|
|
280
|
+
if let Some(node) = queue.pop_front() {
|
|
281
|
+
batch.insert(node.clone());
|
|
282
|
+
processed += 1;
|
|
283
|
+
|
|
284
|
+
if let Some(deps) = subgraph.get(&node) {
|
|
285
|
+
for dep in deps {
|
|
286
|
+
if let Some(degree) = in_degree.get_mut(dep) {
|
|
287
|
+
*degree -= 1;
|
|
288
|
+
if *degree == 0 {
|
|
289
|
+
queue.push_back(dep.clone());
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if !batch.is_empty() {
|
|
298
|
+
batches.push(batch);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if processed < total {
|
|
303
|
+
let unprocessed: Vec<String> = subgraph
|
|
304
|
+
.keys()
|
|
305
|
+
.filter(|k| in_degree.get(*k).is_some_and(|&d| d > 0))
|
|
306
|
+
.cloned()
|
|
307
|
+
.collect();
|
|
308
|
+
|
|
309
|
+
if let Some(start) = unprocessed.first() {
|
|
310
|
+
let mut cycle = vec![start.clone()];
|
|
311
|
+
let mut current = start;
|
|
312
|
+
let mut visited_in_path = HashSet::new();
|
|
313
|
+
visited_in_path.insert(start.clone());
|
|
314
|
+
|
|
315
|
+
while let Some(deps) = subgraph.get(current) {
|
|
316
|
+
if let Some(next) = deps.iter().find(|d| unprocessed.contains(d)) {
|
|
317
|
+
if visited_in_path.contains(next) {
|
|
318
|
+
cycle.push(next.clone());
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
cycle.push(next.clone());
|
|
322
|
+
visited_in_path.insert(next.clone());
|
|
323
|
+
current = next;
|
|
324
|
+
} else {
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if cycle.len() > 1
|
|
330
|
+
&& let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s)
|
|
331
|
+
{
|
|
332
|
+
cycle.rotate_left(min_idx);
|
|
333
|
+
if let Some(first) = cycle.first().cloned()
|
|
334
|
+
&& let Some(last) = cycle.last_mut()
|
|
335
|
+
{
|
|
336
|
+
*last = first;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return Err(DependencyError::CircularDependency { cycle });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return Err(DependencyError::CircularDependency { cycle: unprocessed });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
batches.reverse();
|
|
347
|
+
|
|
348
|
+
Ok(batches)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[cfg(test)]
|
|
353
|
+
mod tests {
|
|
354
|
+
use super::*;
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
fn test_new() {
|
|
358
|
+
let graph = DependencyGraph::new();
|
|
359
|
+
assert_eq!(graph.graph.len(), 0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#[test]
|
|
363
|
+
fn test_add_dependency_simple() {
|
|
364
|
+
let mut graph = DependencyGraph::new();
|
|
365
|
+
assert!(graph.add_dependency("a", vec![]).is_ok());
|
|
366
|
+
assert!(graph.graph.contains_key("a"));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn test_add_dependency_duplicate() {
|
|
371
|
+
let mut graph = DependencyGraph::new();
|
|
372
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
373
|
+
let result = graph.add_dependency("a", vec![]);
|
|
374
|
+
assert!(matches!(result, Err(DependencyError::DuplicateKey { .. })));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#[test]
|
|
378
|
+
fn test_has_cycle_simple() {
|
|
379
|
+
let mut graph = DependencyGraph::new();
|
|
380
|
+
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
381
|
+
|
|
382
|
+
assert!(graph.has_cycle_with("b", &["a".to_string()]));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn test_has_cycle_complex() {
|
|
387
|
+
let mut graph = DependencyGraph::new();
|
|
388
|
+
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
389
|
+
graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
390
|
+
|
|
391
|
+
assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn test_has_cycle_self_loop() {
|
|
396
|
+
let graph = DependencyGraph::new();
|
|
397
|
+
|
|
398
|
+
assert!(graph.has_cycle_with("a", &["a".to_string()]));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
#[test]
|
|
402
|
+
fn test_no_cycle() {
|
|
403
|
+
let mut graph = DependencyGraph::new();
|
|
404
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
405
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
406
|
+
|
|
407
|
+
assert!(!graph.has_cycle_with("c", &["a".to_string()]));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#[test]
|
|
411
|
+
fn test_calculate_batches_simple() {
|
|
412
|
+
let mut graph = DependencyGraph::new();
|
|
413
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
414
|
+
|
|
415
|
+
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
416
|
+
assert_eq!(batches.len(), 1);
|
|
417
|
+
assert!(batches[0].contains("a"));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn test_calculate_batches_linear() {
|
|
422
|
+
let mut graph = DependencyGraph::new();
|
|
423
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
424
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
425
|
+
graph.add_dependency("c", vec!["b".to_string()]).unwrap();
|
|
426
|
+
|
|
427
|
+
let batches = graph
|
|
428
|
+
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
429
|
+
.unwrap();
|
|
430
|
+
|
|
431
|
+
assert_eq!(batches.len(), 3);
|
|
432
|
+
assert!(batches[0].contains("a"));
|
|
433
|
+
assert!(batches[1].contains("b"));
|
|
434
|
+
assert!(batches[2].contains("c"));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[test]
|
|
438
|
+
fn test_calculate_batches_parallel() {
|
|
439
|
+
let mut graph = DependencyGraph::new();
|
|
440
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
441
|
+
graph.add_dependency("b", vec![]).unwrap();
|
|
442
|
+
graph
|
|
443
|
+
.add_dependency("c", vec!["a".to_string(), "b".to_string()])
|
|
444
|
+
.unwrap();
|
|
445
|
+
|
|
446
|
+
let batches = graph
|
|
447
|
+
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
448
|
+
.unwrap();
|
|
449
|
+
|
|
450
|
+
assert_eq!(batches.len(), 2);
|
|
451
|
+
assert_eq!(batches[0].len(), 2);
|
|
452
|
+
assert!(batches[0].contains("a"));
|
|
453
|
+
assert!(batches[0].contains("b"));
|
|
454
|
+
assert!(batches[1].contains("c"));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#[test]
|
|
458
|
+
fn test_calculate_batches_nested() {
|
|
459
|
+
let mut graph = DependencyGraph::new();
|
|
460
|
+
graph.add_dependency("config", vec![]).unwrap();
|
|
461
|
+
graph.add_dependency("database", vec!["config".to_string()]).unwrap();
|
|
462
|
+
graph.add_dependency("cache", vec!["config".to_string()]).unwrap();
|
|
463
|
+
graph
|
|
464
|
+
.add_dependency("service", vec!["database".to_string(), "cache".to_string()])
|
|
465
|
+
.unwrap();
|
|
466
|
+
|
|
467
|
+
let batches = graph
|
|
468
|
+
.calculate_batches(&[
|
|
469
|
+
"config".to_string(),
|
|
470
|
+
"database".to_string(),
|
|
471
|
+
"cache".to_string(),
|
|
472
|
+
"service".to_string(),
|
|
473
|
+
])
|
|
474
|
+
.unwrap();
|
|
475
|
+
|
|
476
|
+
assert_eq!(batches.len(), 3);
|
|
477
|
+
assert!(batches[0].contains("config"));
|
|
478
|
+
assert_eq!(batches[1].len(), 2);
|
|
479
|
+
assert!(batches[1].contains("database"));
|
|
480
|
+
assert!(batches[1].contains("cache"));
|
|
481
|
+
assert!(batches[2].contains("service"));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
#[test]
|
|
485
|
+
fn test_calculate_batches_partial() {
|
|
486
|
+
let mut graph = DependencyGraph::new();
|
|
487
|
+
graph.add_dependency("a", vec![]).unwrap();
|
|
488
|
+
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
489
|
+
graph.add_dependency("c", vec!["a".to_string()]).unwrap();
|
|
490
|
+
|
|
491
|
+
let batches = graph.calculate_batches(&["b".to_string()]).unwrap();
|
|
492
|
+
|
|
493
|
+
assert_eq!(batches.len(), 2);
|
|
494
|
+
assert!(batches[0].contains("a"));
|
|
495
|
+
assert!(batches[1].contains("b"));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
fn test_calculate_batches_missing_dependency() {
|
|
500
|
+
let mut graph = DependencyGraph::new();
|
|
501
|
+
graph.add_dependency("a", vec!["missing".to_string()]).unwrap();
|
|
502
|
+
|
|
503
|
+
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
504
|
+
assert!(!batches.is_empty());
|
|
505
|
+
}
|
|
506
|
+
}
|