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
9/// Unified trait-kit error type.
10///
11/// Follows the `ProjectNameError` naming convention used across the base workspace.
12///
13/// `Display` 实现通过 [`tr`] 查询 Fluent 消息目录,自动根据当前 locale 输出对应语言文本。
14#[derive(Debug)]
15pub enum TraitKitError {
16    /// 依赖图中检测到环。
17    CycleDetected {
18        /// 环路上的模块名称序列。
19        cycle: Vec<&'static str>,
20    },
21
22    /// 依赖的模块未注册。
23    DependencyMissing {
24        /// 发起依赖的模块。
25        module: &'static str,
26        /// 缺失的依赖模块。
27        missing: &'static str,
28    },
29
30    /// 模块重复注册。
31    AlreadyRegistered {
32        /// 重复注册的模块名。
33        module: &'static str,
34    },
35
36    /// 模块构建失败。
37    BuildFailed {
38        /// 构建失败的上下文描述。
39        context: &'static str,
40        /// 底层错误源。
41        source: Box<dyn std::error::Error + Send + 'static>,
42    },
43
44    /// 请求的能力不存在。
45    MissingCapability {
46        /// 缺失的能力标识。
47        key: &'static str,
48    },
49
50    /// 请求的配置不存在。
51    MissingConfig {
52        /// 缺失的配置键。
53        key: &'static str,
54    },
55
56    /// 生命周期钩子执行失败。
57    #[cfg(feature = "lifecycle")]
58    LifecycleFailed {
59        /// 钩子所属模块的上下文描述。
60        context: &'static str,
61        /// 底层错误源。
62        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
148/// Convenience `Result` alias for trait-kit operations.
149///
150/// Provided for ergonomic use in downstream crates.
151pub 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}