Skip to main content

injectable_rs_graph/
validate.rs

1//! Validation error types for the dependency graph.
2
3/// Errors found during dependency graph validation.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum ValidationError {
6    /// A circular dependency was detected.
7    ///
8    /// The `chain` field shows the cycle path, e.g.:
9    /// `["UserService", "AuthService", "SessionManager", "UserService"]`
10    CircularDependency {
11        /// The chain of types forming the cycle.
12        chain: Vec<String>,
13    },
14
15    /// A dependency references a type not registered in the graph.
16    MissingDependency {
17        /// The type that has the missing dependency.
18        source: String,
19        /// The missing dependency type name.
20        missing: String,
21    },
22
23    /// The same type name appears more than once in the graph.
24    DuplicateNode {
25        /// The duplicated type name.
26        name: String,
27    },
28
29    /// A type has multiple constructors annotated with `#[injectable(ctor)]`.
30    MultipleConstructors {
31        /// The type with multiple constructors.
32        type_name: String,
33        /// The number of constructors found.
34        count: usize,
35    },
36
37    /// A type has duplicate lifecycle hooks.
38    DuplicateLifecycleHook {
39        /// The type with duplicate hooks.
40        type_name: String,
41        /// Which hook is duplicated.
42        hook: String,
43    },
44
45    /// A constructor has an invalid return type.
46    InvalidConstructorReturn {
47        /// The type with the invalid constructor.
48        type_name: String,
49        /// The expected return type.
50        expected: String,
51    },
52
53    /// A scope mismatch: a wider-scope type depends on a narrower-scope type.
54    ///
55    /// For example, a singleton depending on a transient would capture the
56    /// transient instance forever, violating transient semantics.
57    ScopeMismatch {
58        /// The type with the wider scope (e.g., singleton).
59        source: String,
60        /// The scope of the source type.
61        source_scope: String,
62        /// The dependency with the narrower scope (e.g., transient).
63        dependency: String,
64        /// The scope of the dependency.
65        dependency_scope: String,
66    },
67}
68
69impl std::fmt::Display for ValidationError {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Self::CircularDependency { chain } => {
73                write!(f, "circular dependency detected: ")?;
74                for (i, t) in chain.iter().enumerate() {
75                    if i > 0 {
76                        write!(f, " -> ")?;
77                    }
78                    write!(f, "{t}")?;
79                }
80                Ok(())
81            }
82            Self::MissingDependency { source, missing } => {
83                write!(
84                    f,
85                    "`{source}` depends on `{missing}`, which is not registered"
86                )
87            }
88            Self::DuplicateNode { name } => {
89                write!(f, "duplicate node definition for `{name}`")
90            }
91            Self::MultipleConstructors { type_name, count } => {
92                write!(
93                    f,
94                    "`{type_name}` has {count} constructors; expected exactly 1"
95                )
96            }
97            Self::DuplicateLifecycleHook { type_name, hook } => {
98                write!(f, "`{type_name}` has duplicate `{hook}` hooks")
99            }
100            Self::InvalidConstructorReturn {
101                type_name,
102                expected,
103            } => {
104                write!(f, "constructor for `{type_name}` must return `{expected}`")
105            }
106            Self::ScopeMismatch {
107                source,
108                source_scope,
109                dependency,
110                dependency_scope,
111            } => {
112                write!(
113                    f,
114                    "scope mismatch: `{source}` ({source_scope}) depends on `{dependency}` ({dependency_scope}); \
115                     wider-scope types cannot depend on narrower-scope types"
116                )
117            }
118        }
119    }
120}
121
122impl std::error::Error for ValidationError {}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn circular_dependency_display() {
130        let e = ValidationError::CircularDependency {
131            chain: vec!["A".to_string(), "B".to_string(), "A".to_string()],
132        };
133        let s = e.to_string();
134        assert!(s.contains("circular dependency"));
135        assert!(s.contains("A -> B -> A"));
136    }
137
138    #[test]
139    fn missing_dependency_display() {
140        let e = ValidationError::MissingDependency {
141            source: "UserService".to_string(),
142            missing: "Database".to_string(),
143        };
144        let s = e.to_string();
145        assert!(s.contains("UserService"));
146        assert!(s.contains("Database"));
147        assert!(s.contains("not registered"));
148    }
149
150    #[test]
151    fn duplicate_node_display() {
152        let e = ValidationError::DuplicateNode {
153            name: "Cache".to_string(),
154        };
155        let s = e.to_string();
156        assert!(s.contains("duplicate"));
157        assert!(s.contains("Cache"));
158    }
159
160    #[test]
161    fn multiple_constructors_display() {
162        let e = ValidationError::MultipleConstructors {
163            type_name: "Foo".to_string(),
164            count: 3,
165        };
166        let s = e.to_string();
167        assert!(s.contains("Foo"));
168        assert!(s.contains("3"));
169    }
170
171    #[test]
172    fn duplicate_lifecycle_hook_display() {
173        let e = ValidationError::DuplicateLifecycleHook {
174            type_name: "Bar".to_string(),
175            hook: "post_construct".to_string(),
176        };
177        let s = e.to_string();
178        assert!(s.contains("Bar"));
179        assert!(s.contains("post_construct"));
180    }
181
182    #[test]
183    fn invalid_constructor_return_display() {
184        let e = ValidationError::InvalidConstructorReturn {
185            type_name: "Baz".to_string(),
186            expected: "Self".to_string(),
187        };
188        let s = e.to_string();
189        assert!(s.contains("Baz"));
190        assert!(s.contains("Self"));
191    }
192
193    #[test]
194    fn scope_mismatch_display() {
195        let e = ValidationError::ScopeMismatch {
196            source: "Singleton".to_string(),
197            source_scope: "singleton".to_string(),
198            dependency: "Transient".to_string(),
199            dependency_scope: "transient".to_string(),
200        };
201        let s = e.to_string();
202        assert!(s.contains("scope mismatch"));
203        assert!(s.contains("singleton"));
204        assert!(s.contains("transient"));
205    }
206
207    #[test]
208    fn error_trait_impl() {
209        let e = ValidationError::DuplicateNode {
210            name: "X".to_string(),
211        };
212        let _: &dyn std::error::Error = &e;
213    }
214
215    #[test]
216    fn clone_and_eq() {
217        let a = ValidationError::DuplicateNode {
218            name: "A".to_string(),
219        };
220        let b = a.clone();
221        assert_eq!(a, b);
222    }
223}