Skip to main content

injectable_rs_graph/
graph.rs

1//! The dependency graph — a collection of nodes with validation.
2
3use crate::{GraphNode, ValidationError};
4
5/// A dependency graph of all injectable types in the application.
6///
7/// The graph is constructed from metadata generated by the proc macros
8/// and is validated once at container build time. After validation,
9/// the graph is not used during runtime resolution — providers resolve
10/// dependencies through static dispatch.
11///
12/// # Construction
13///
14/// The graph is typically built automatically by the container builder:
15///
16/// ```rust,ignore
17/// let graph = DependencyGraph::new(vec![
18///     GraphNode::new("UserService", &["Database", "Cache"]),
19///     GraphNode::leaf("Database"),
20///     GraphNode::leaf("Cache"),
21/// ]);
22/// graph.validate()?;
23/// ```
24#[derive(Debug, Clone)]
25pub struct DependencyGraph {
26    nodes: Vec<GraphNode>,
27}
28
29impl DependencyGraph {
30    /// Create a new empty dependency graph.
31    pub fn empty() -> Self {
32        Self { nodes: Vec::new() }
33    }
34
35    /// Create a new dependency graph from a list of nodes.
36    pub fn new(nodes: Vec<GraphNode>) -> Self {
37        Self { nodes }
38    }
39
40    /// Add a node to the graph.
41    pub fn add_node(&mut self, node: GraphNode) {
42        self.nodes.push(node);
43    }
44
45    /// Get all nodes in the graph.
46    pub fn nodes(&self) -> &[GraphNode] {
47        &self.nodes
48    }
49
50    /// Get the number of nodes in the graph.
51    pub fn len(&self) -> usize {
52        self.nodes.len()
53    }
54
55    /// Returns `true` if the graph contains no nodes.
56    pub fn is_empty(&self) -> bool {
57        self.nodes.is_empty()
58    }
59
60    /// Find a node by name.
61    pub fn find_node(&self, name: &str) -> Option<&GraphNode> {
62        self.nodes.iter().find(|n| n.name == name)
63    }
64
65    /// Validate the entire dependency graph.
66    ///
67    /// Checks for:
68    /// - Circular dependencies (via DFS)
69    /// - Missing dependencies (references to types not in the graph)
70    /// - Duplicate node definitions
71    /// - Scope mismatches (wider-scope types depending on narrower-scope types)
72    ///
73    /// Returns a list of validation errors. An empty list means the graph is valid.
74    pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
75        let mut errors = Vec::new();
76
77        // Check for duplicate nodes
78        self.check_duplicates(&mut errors);
79
80        // Check for missing dependencies
81        self.check_missing(&mut errors);
82
83        // Check for circular dependencies
84        self.check_cycles(&mut errors);
85
86        // Check for scope mismatches
87        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                    // Skip path-qualified names (containing `::` or `<`): these are
114                    // external types provided via DynProvider and are not in the graph.
115                    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                    // Found a cycle — record the cycle path
157                    let cycle_start = path.iter().position(|n| *n == *dep).unwrap_or(0);
158                    // Panic Safety: cycle_start comes from position() which returns an index < path.len(),
159                    // or 0 as fallback; either way cycle_start <= path.len().
160                    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    /// Check for scope mismatches.
178    ///
179    /// A scope mismatch occurs when a wider-scope type (e.g., singleton)
180    /// depends on a narrower-scope type (e.g., transient). This is
181    /// problematic because the narrower-scope instance would be captured
182    /// by the wider-scope instance for its entire lifetime, violating
183    /// the narrower scope's semantics.
184    ///
185    /// # Scope Ordering
186    ///
187    /// From widest to narrowest:
188    /// - `singleton` (widest — lives for the entire application lifetime)
189    /// - `transient` (narrowest — new instance per resolution)
190    ///
191    /// # Valid Combinations
192    ///
193    /// - singleton → singleton: OK
194    /// - transient → singleton: OK (transient gets a shared singleton)
195    /// - transient → transient: OK (each gets a fresh instance)
196    /// - singleton → transient: ERROR (singleton captures a single transient forever)
197    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    /// Compute the topological order of the dependency graph.
215    ///
216    /// Returns nodes in construction order (dependencies before dependents).
217    /// Returns an error if the graph contains cycles.
218    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; // Cycle — already validated above
246        }
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    /// Compute the destruction order (reverse topological).
262    ///
263    /// Dependencies are destroyed after their dependents.
264    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
271/// Determine if `source_scope` is wider than `dep_scope`.
272///
273/// Returns `true` when a scope mismatch would occur — i.e., when a
274/// wider-scope type depends on a narrower-scope type.
275///
276/// # Scope Width Ordering
277///
278/// - `singleton` is the widest scope
279/// - `transient` is narrower than `singleton`
280///
281/// Any unrecognized scope is treated as equivalent to `singleton` (no mismatch).
282fn is_wider_scope(source_scope: &str, dep_scope: &str) -> bool {
283    match (source_scope, dep_scope) {
284        // singleton depending on transient is the canonical mismatch
285        ("singleton", "transient") => true,
286        // All other combinations are valid
287        _ => 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        // Dependencies with '::' are external types, not graph nodes
426        let g = DependencyGraph::new(vec![GraphNode::new("MyService", &["sqlx::SqlitePool"])]);
427        assert!(g.validate().is_ok());
428    }
429}
430
431// Helper for tests: create a node with a scope override via builder pattern
432#[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}