1use crate::{GraphNode, ValidationError};
4
5#[derive(Debug, Clone)]
25pub struct DependencyGraph {
26 nodes: Vec<GraphNode>,
27}
28
29impl DependencyGraph {
30 pub fn empty() -> Self {
32 Self { nodes: Vec::new() }
33 }
34
35 pub fn new(nodes: Vec<GraphNode>) -> Self {
37 Self { nodes }
38 }
39
40 pub fn add_node(&mut self, node: GraphNode) {
42 self.nodes.push(node);
43 }
44
45 pub fn nodes(&self) -> &[GraphNode] {
47 &self.nodes
48 }
49
50 pub fn len(&self) -> usize {
52 self.nodes.len()
53 }
54
55 pub fn is_empty(&self) -> bool {
57 self.nodes.is_empty()
58 }
59
60 pub fn find_node(&self, name: &str) -> Option<&GraphNode> {
62 self.nodes.iter().find(|n| n.name == name)
63 }
64
65 pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
75 let mut errors = Vec::new();
76
77 self.check_duplicates(&mut errors);
79
80 self.check_missing(&mut errors);
82
83 self.check_cycles(&mut errors);
85
86 self.check_scope_mismatches(&mut errors);
88
89 if errors.is_empty() {
90 Ok(())
91 } else {
92 Err(errors)
93 }
94 }
95
96 fn check_duplicates(&self, errors: &mut Vec<ValidationError>) {
97 let mut seen = std::collections::HashSet::new();
98 for node in &self.nodes {
99 if !seen.insert(node.name) {
100 errors.push(ValidationError::DuplicateNode {
101 name: node.name.to_string(),
102 });
103 }
104 }
105 }
106
107 fn check_missing(&self, errors: &mut Vec<ValidationError>) {
108 let names: std::collections::HashSet<&str> = self.nodes.iter().map(|n| n.name).collect();
109
110 for node in &self.nodes {
111 for dep in node.dependencies {
112 if !names.contains(dep) {
113 if dep.contains("::") || dep.contains('<') {
116 continue;
117 }
118 errors.push(ValidationError::MissingDependency {
119 source: node.name.to_string(),
120 missing: dep.to_string(),
121 });
122 }
123 }
124 }
125 }
126
127 fn check_cycles(&self, errors: &mut Vec<ValidationError>) {
128 let mut visited = std::collections::HashSet::new();
129 let mut in_stack = std::collections::HashSet::new();
130 let mut path = Vec::new();
131
132 for node in &self.nodes {
133 if !visited.contains(node.name) {
134 self.dfs(node.name, &mut visited, &mut in_stack, &mut path, errors);
135 }
136 }
137 }
138
139 fn dfs<'a>(
140 &self,
141 current: &'a str,
142 visited: &mut std::collections::HashSet<&'a str>,
143 in_stack: &mut std::collections::HashSet<&'a str>,
144 path: &mut Vec<&'a str>,
145 errors: &mut Vec<ValidationError>,
146 ) {
147 visited.insert(current);
148 in_stack.insert(current);
149 path.push(current);
150
151 if let Some(node) = self.find_node(current) {
152 for dep in node.dependencies {
153 if !visited.contains(dep) {
154 self.dfs(dep, visited, in_stack, path, errors);
155 } else if in_stack.contains(dep) {
156 let cycle_start = path.iter().position(|n| *n == *dep).unwrap_or(0);
158 let cycle: Vec<String> = path
161 .get(cycle_start..)
162 .unwrap_or(&[])
163 .iter()
164 .map(|s| s.to_string())
165 .chain(std::iter::once(dep.to_string()))
166 .collect();
167
168 errors.push(ValidationError::CircularDependency { chain: cycle });
169 }
170 }
171 }
172
173 path.pop();
174 in_stack.remove(current);
175 }
176
177 fn check_scope_mismatches(&self, errors: &mut Vec<ValidationError>) {
198 for node in &self.nodes {
199 for dep_name in node.dependencies {
200 if let Some(dep) = self.find_node(dep_name) {
201 if is_wider_scope(node.scope, dep.scope) {
202 errors.push(ValidationError::ScopeMismatch {
203 source: node.name.to_string(),
204 source_scope: node.scope.to_string(),
205 dependency: dep_name.to_string(),
206 dependency_scope: dep.scope.to_string(),
207 });
208 }
209 }
210 }
211 }
212 }
213
214 pub fn topological_order(&self) -> Result<Vec<&str>, Vec<ValidationError>> {
219 self.validate()?;
220
221 let mut result = Vec::new();
222 let mut visited = std::collections::HashSet::new();
223 let mut temp_marked = std::collections::HashSet::new();
224
225 for node in &self.nodes {
226 if !visited.contains(node.name) {
227 self.topo_visit(node.name, &mut visited, &mut temp_marked, &mut result);
228 }
229 }
230
231 Ok(result)
232 }
233
234 fn topo_visit<'a>(
235 &self,
236 current: &'a str,
237 visited: &mut std::collections::HashSet<&'a str>,
238 temp_marked: &mut std::collections::HashSet<&'a str>,
239 result: &mut Vec<&'a str>,
240 ) {
241 if visited.contains(current) {
242 return;
243 }
244 if temp_marked.contains(current) {
245 return; }
247
248 temp_marked.insert(current);
249
250 if let Some(node) = self.find_node(current) {
251 for dep in node.dependencies {
252 self.topo_visit(dep, visited, temp_marked, result);
253 }
254 }
255
256 temp_marked.remove(current);
257 visited.insert(current);
258 result.push(current);
259 }
260
261 pub fn destruction_order(&self) -> Result<Vec<&str>, Vec<ValidationError>> {
265 let mut order = self.topological_order()?;
266 order.reverse();
267 Ok(order)
268 }
269}
270
271fn is_wider_scope(source_scope: &str, dep_scope: &str) -> bool {
283 match (source_scope, dep_scope) {
284 ("singleton", "transient") => true,
286 _ => false,
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_is_wider_scope() {
297 assert!(is_wider_scope("singleton", "transient"));
298 assert!(!is_wider_scope("singleton", "singleton"));
299 assert!(!is_wider_scope("transient", "singleton"));
300 assert!(!is_wider_scope("transient", "transient"));
301 assert!(!is_wider_scope("request", "transient"));
302 assert!(!is_wider_scope("singleton", "request"));
303 }
304
305 #[test]
306 fn empty_graph_is_valid() {
307 let g = DependencyGraph::empty();
308 assert!(g.is_empty());
309 assert_eq!(g.len(), 0);
310 assert!(g.validate().is_ok());
311 }
312
313 #[test]
314 fn valid_linear_graph() {
315 let g = DependencyGraph::new(vec![
316 GraphNode::leaf("Database"),
317 GraphNode::new("UserService", &["Database"]),
318 ]);
319 assert!(g.validate().is_ok());
320 }
321
322 #[test]
323 fn circular_dependency_detected() {
324 let g = DependencyGraph::new(vec![
325 GraphNode::new("A", &["B"]),
326 GraphNode::new("B", &["A"]),
327 ]);
328 let errs = g.validate().unwrap_err();
329 assert!(
330 errs.iter()
331 .any(|e| matches!(e, ValidationError::CircularDependency { .. }))
332 );
333 }
334
335 #[test]
336 fn three_node_cycle_detected() {
337 let g = DependencyGraph::new(vec![
338 GraphNode::new("A", &["B"]),
339 GraphNode::new("B", &["C"]),
340 GraphNode::new("C", &["A"]),
341 ]);
342 let errs = g.validate().unwrap_err();
343 assert!(
344 errs.iter()
345 .any(|e| matches!(e, ValidationError::CircularDependency { .. }))
346 );
347 }
348
349 #[test]
350 fn missing_dependency_detected() {
351 let g = DependencyGraph::new(vec![GraphNode::new("UserService", &["MissingDep"])]);
352 let errs = g.validate().unwrap_err();
353 assert!(
354 errs.iter()
355 .any(|e| matches!(e, ValidationError::MissingDependency { .. }))
356 );
357 }
358
359 #[test]
360 fn duplicate_node_detected() {
361 let g = DependencyGraph::new(vec![
362 GraphNode::leaf("Database"),
363 GraphNode::leaf("Database"),
364 ]);
365 let errs = g.validate().unwrap_err();
366 assert!(
367 errs.iter()
368 .any(|e| matches!(e, ValidationError::DuplicateNode { .. }))
369 );
370 }
371
372 #[test]
373 fn scope_mismatch_detected() {
374 let g = DependencyGraph::new(vec![
375 GraphNode::leaf("Transient").then_with_scope("transient"),
376 GraphNode::with_scope("Singleton", &["Transient"], "singleton"),
377 ]);
378 let errs = g.validate().unwrap_err();
379 assert!(
380 errs.iter()
381 .any(|e| matches!(e, ValidationError::ScopeMismatch { .. }))
382 );
383 }
384
385 #[test]
386 fn topological_order_valid_graph() {
387 let g = DependencyGraph::new(vec![
388 GraphNode::leaf("Database"),
389 GraphNode::new("UserService", &["Database"]),
390 ]);
391 let order = g.topological_order().unwrap();
392 let db_pos = order.iter().position(|n| *n == "Database").unwrap();
393 let svc_pos = order.iter().position(|n| *n == "UserService").unwrap();
394 assert!(db_pos < svc_pos);
395 }
396
397 #[test]
398 fn destruction_order_is_reverse_topo() {
399 let g = DependencyGraph::new(vec![
400 GraphNode::leaf("Database"),
401 GraphNode::new("UserService", &["Database"]),
402 ]);
403 let topo = g.topological_order().unwrap();
404 let destruct = g.destruction_order().unwrap();
405 assert_eq!(topo, destruct.iter().rev().cloned().collect::<Vec<_>>());
406 }
407
408 #[test]
409 fn find_node_existing() {
410 let g = DependencyGraph::new(vec![GraphNode::leaf("Database")]);
411 assert!(g.find_node("Database").is_some());
412 assert!(g.find_node("Missing").is_none());
413 }
414
415 #[test]
416 fn add_node_increases_len() {
417 let mut g = DependencyGraph::empty();
418 g.add_node(GraphNode::leaf("Foo"));
419 assert_eq!(g.len(), 1);
420 assert!(!g.is_empty());
421 }
422
423 #[test]
424 fn path_qualified_dep_not_missing() {
425 let g = DependencyGraph::new(vec![GraphNode::new("MyService", &["sqlx::SqlitePool"])]);
427 assert!(g.validate().is_ok());
428 }
429}
430
431#[allow(dead_code)]
433trait NodeScopeExt {
434 fn then_with_scope(self, scope: &'static str) -> GraphNode;
435}
436
437impl NodeScopeExt for GraphNode {
438 fn then_with_scope(self, scope: &'static str) -> GraphNode {
439 GraphNode::with_scope(self.name, self.dependencies, scope)
440 }
441}