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