1use std::fmt;
6
7use crate::i18n::tr;
8
9#[derive(Debug)]
15pub enum TraitKitError {
16 CycleDetected {
18 cycle: Vec<&'static str>,
20 },
21
22 DependencyMissing {
24 module: &'static str,
26 missing: &'static str,
28 },
29
30 AlreadyRegistered {
32 module: &'static str,
34 },
35
36 BuildFailed {
38 context: &'static str,
40 source: Box<dyn std::error::Error + Send + 'static>,
42 },
43
44 MissingCapability {
46 key: &'static str,
48 },
49
50 MissingConfig {
52 key: &'static str,
54 },
55
56 #[cfg(feature = "lifecycle")]
58 LifecycleFailed {
59 context: &'static str,
61 source: Box<dyn std::error::Error + Send + 'static>,
63 },
64}
65
66impl fmt::Display for TraitKitError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::CycleDetected { cycle } => {
70 write!(
71 f,
72 "{}",
73 tr(
74 "trait-kit-error-cycle-detected",
75 &[("cycle", &cycle.join(" → "))],
76 )
77 )
78 }
79 Self::DependencyMissing { module, missing } => {
80 write!(
81 f,
82 "{}",
83 tr(
84 "trait-kit-error-dependency-missing",
85 &[("module", *module), ("missing", *missing)],
86 )
87 )
88 }
89 Self::AlreadyRegistered { module } => {
90 write!(
91 f,
92 "{}",
93 tr("trait-kit-error-already-registered", &[("module", *module)]),
94 )
95 }
96 Self::BuildFailed { context, source } => {
97 let source_str = source.to_string();
98 write!(
99 f,
100 "{}",
101 tr(
102 "trait-kit-error-build-failed",
103 &[("context", *context), ("source", &source_str)],
104 )
105 )
106 }
107 Self::MissingCapability { key } => {
108 write!(
109 f,
110 "{}",
111 tr("trait-kit-error-missing-capability", &[("key", *key)]),
112 )
113 }
114 Self::MissingConfig { key } => {
115 write!(
116 f,
117 "{}",
118 tr("trait-kit-error-missing-config", &[("key", *key)]),
119 )
120 }
121 #[cfg(feature = "lifecycle")]
122 Self::LifecycleFailed { context, source } => {
123 let source_str = source.to_string();
124 write!(
125 f,
126 "{}",
127 tr(
128 "trait-kit-error-lifecycle-failed",
129 &[("context", *context), ("source", &source_str)],
130 )
131 )
132 }
133 }
134 }
135}
136
137impl std::error::Error for TraitKitError {
138 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
139 match self {
140 Self::BuildFailed { source, .. } => Some(source.as_ref()),
141 #[cfg(feature = "lifecycle")]
142 Self::LifecycleFailed { source, .. } => Some(source.as_ref()),
143 _ => None,
144 }
145 }
146}
147
148pub type TraitKitResult<T> = std::result::Result<T, TraitKitError>;
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn cycle_detected_display_contains_modules() {
159 let err = TraitKitError::CycleDetected {
160 cycle: vec!["A", "B", "C"],
161 };
162 let msg = format!("{err}");
163 assert!(msg.contains('A'), "should contain module A: got '{msg}'");
164 assert!(msg.contains('B'), "should contain module B: got '{msg}'");
165 assert!(msg.contains('C'), "should contain module C: got '{msg}'");
166 }
167
168 #[test]
169 fn dependency_missing_display_contains_both_modules() {
170 let err = TraitKitError::DependencyMissing {
171 module: "mod-a",
172 missing: "mod-b",
173 };
174 let msg = format!("{err}");
175 assert!(msg.contains("mod-a"), "should contain module: got '{msg}'");
176 assert!(
177 msg.contains("mod-b"),
178 "should contain missing dep: got '{msg}'"
179 );
180 }
181
182 #[test]
183 fn already_registered_display_contains_module() {
184 let err = TraitKitError::AlreadyRegistered {
185 module: "my-module",
186 };
187 let msg = format!("{err}");
188 assert!(
189 msg.contains("my-module"),
190 "should contain module name: got '{msg}'"
191 );
192 }
193
194 #[test]
195 fn build_failed_display_contains_context_and_source() {
196 let err = TraitKitError::BuildFailed {
197 context: "build",
198 source: Box::new(std::io::Error::new(std::io::ErrorKind::Other, "oops")),
199 };
200 let msg = format!("{err}");
201 assert!(msg.contains("build"), "should contain context: got '{msg}'");
202 assert!(
203 msg.contains("oops"),
204 "should contain source error: got '{msg}'"
205 );
206 }
207
208 #[test]
209 fn missing_capability_display_contains_key() {
210 let err = TraitKitError::MissingCapability { key: "cap" };
211 let msg = format!("{err}");
212 assert!(msg.contains("cap"), "should contain key: got '{msg}'");
213 }
214
215 #[test]
216 fn missing_config_display_contains_key() {
217 let err = TraitKitError::MissingConfig { key: "db.url" };
218 let msg = format!("{err}");
219 assert!(
220 msg.contains("db.url"),
221 "should contain config key: got '{msg}'"
222 );
223 }
224
225 #[cfg(feature = "lifecycle")]
226 #[test]
227 fn lifecycle_failed_display_contains_context_and_source() {
228 let err = TraitKitError::LifecycleFailed {
229 context: "on_ready",
230 source: Box::new(std::io::Error::new(std::io::ErrorKind::Other, "fail")),
231 };
232 let msg = format!("{err}");
233 assert!(
234 msg.contains("on_ready"),
235 "should contain context: got '{msg}'"
236 );
237 assert!(msg.contains("fail"), "should contain source: got '{msg}'");
238 }
239
240 #[test]
241 fn error_source_returns_inner_for_build_failed() {
242 let err = TraitKitError::BuildFailed {
243 context: "build",
244 source: Box::new(std::io::Error::new(std::io::ErrorKind::Other, "oops")),
245 };
246 assert!(std::error::Error::source(&err).is_some());
247 }
248
249 #[test]
250 fn error_source_returns_none_for_simple_variants() {
251 let err = TraitKitError::MissingConfig { key: "x" };
252 assert!(std::error::Error::source(&err).is_none());
253 }
254
255 #[cfg(feature = "lifecycle")]
256 #[test]
257 fn error_source_returns_inner_for_lifecycle_failed() {
258 let err = TraitKitError::LifecycleFailed {
259 context: "on_ready",
260 source: Box::new(std::io::Error::new(std::io::ErrorKind::Other, "fail")),
261 };
262 assert!(std::error::Error::source(&err).is_some());
263 }
264
265 #[test]
266 fn error_debug_format() {
267 let err = TraitKitError::MissingConfig { key: "x" };
268 let debug = format!("{err:?}");
269 assert!(debug.contains("MissingConfig"));
270 }
271}