Skip to main content

trait_kit/
error.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Error types for Kit operations.
4
5use std::fmt;
6
7use crate::i18n::tr;
8#[cfg(feature = "shutdown")]
9use crate::kit::shutdown::ShutdownPhase;
10
11/// Unified trait-kit error type.
12///
13/// Follows the `ProjectNameError` naming convention used across the base workspace.
14///
15/// `Display` 实现通过 [`tr`] 查询 Fluent 消息目录,自动根据当前 locale 输出对应语言文本。
16#[derive(Debug)]
17pub enum TraitKitError {
18    /// 依赖图中检测到环。
19    CycleDetected {
20        /// 环路上的模块名称序列。
21        cycle: Vec<&'static str>,
22    },
23
24    /// 依赖的模块未注册。
25    DependencyMissing {
26        /// 发起依赖的模块。
27        module: &'static str,
28        /// 缺失的依赖模块。
29        missing: &'static str,
30    },
31
32    /// 模块重复注册。
33    AlreadyRegistered {
34        /// 重复注册的模块名。
35        module: &'static str,
36    },
37
38    /// 模块构建失败。
39    BuildFailed {
40        /// 构建失败的上下文描述(支持 i18n 翻译后的文本)。
41        context: String,
42        /// 底层错误源。
43        source: Box<dyn std::error::Error + Send + 'static>,
44    },
45
46    /// 请求的能力不存在。
47    MissingCapability {
48        /// 缺失的能力标识(支持 i18n 翻译后的文本)。
49        key: String,
50    },
51
52    /// 请求的配置不存在。
53    MissingConfig {
54        /// 缺失的配置键(支持 i18n 翻译后的文本)。
55        key: String,
56    },
57
58    /// 生命周期钩子执行失败。
59    #[cfg(feature = "lifecycle")]
60    LifecycleFailed {
61        /// 钩子所属模块的上下文描述(支持 i18n 翻译后的文本)。
62        context: String,
63        /// 底层错误源。
64        source: Box<dyn std::error::Error + Send + 'static>,
65    },
66
67    /// 优雅关闭超时。
68    #[cfg(feature = "shutdown")]
69    ShutdownTimedOut {
70        /// 超时的关闭阶段列表。
71        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
172/// Convenience `Result` alias for trait-kit operations.
173///
174/// Provided for ergonomic use in downstream crates.
175pub 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}