Skip to main content

xyz_rust/
errors.rs

1// 错误分类学是三个前端共享的单一错误模型:一个带 Kind 的错误同时驱动
2// CLI 退出码、HTTP 状态码与 MCP JSON-RPC 错误码,传输实现永远不需要去
3// 解释命令专属的错误字符串。
4//
5// 零第三方依赖:CodedError 手写 std::error::Error 实现(不引 thiserror)。
6
7use std::fmt;
8
9/// Kind 给错误分类。前端通过 http_status、exit_code、jsonrpc_code 把
10/// Kind 翻译成各自渠道的错误表示。
11#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Kind {
14    /// 调用方提供了畸形或非法的入参(缺必填、校验失败、枚举越界)。
15    InvalidInput,
16    /// 调用方必须先行认证。
17    Unauthorized,
18    /// 调用方已认证但缺少权限。
19    Forbidden,
20    /// 操作目标不存在。
21    NotFound,
22    /// 操作与现有状态冲突。
23    Conflict,
24    /// 操作被调用方取消。
25    Canceled,
26    /// 依赖的服务暂时不可用。
27    Unavailable,
28    /// 未分类失败的兜底分类。
29    Internal,
30}
31
32impl Kind {
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            Kind::InvalidInput => "invalid_input",
36            Kind::Unauthorized => "unauthorized",
37            Kind::Forbidden => "forbidden",
38            Kind::NotFound => "not_found",
39            Kind::Conflict => "conflict",
40            Kind::Canceled => "canceled",
41            Kind::Unavailable => "unavailable",
42            Kind::Internal => "internal",
43        }
44    }
45}
46
47impl fmt::Display for Kind {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.write_str(self.as_str())
50    }
51}
52
53/// Error 是库内统一的错误类型(CodedError 的 Rust 对应物)。
54/// kind 缺省为 Internal;用户在自己的 handler 里也可以直接返回任何
55/// `std::error::Error`(由 classify 兜底分类为 Internal)。
56///
57/// spec §8.5:可选富化层由 builder 链挂载——
58/// `Error::new(NotFound, "user 7").with_code("USER_NOT_FOUND")
59///  .with_detail("user_id", json!(7)).with_status(410)`。
60#[derive(Debug)]
61pub struct Error {
62    kind: Kind,
63    message: String,
64    cause: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
65    /// §8.5 富化层(code/detail/status);Box 保持 Result 的小体积
66    /// (clippy::result_large_err)。
67    rich: Option<Box<Rich>>,
68}
69
70/// §8.5 的三层可选富化上下文。
71#[derive(Debug, Clone, Default)]
72pub struct Rich {
73    /// 自由域标识(绝不参与传输映射)。
74    pub code: Option<String>,
75    /// 结构化上下文,原样进 §8.6 错误体。
76    pub detail: Option<serde_json::Map<String, serde_json::Value>>,
77    /// HTTP 状态覆盖(仅 HTTP 通道)。
78    pub status: Option<u16>,
79}
80
81impl Error {
82    fn base(
83        kind: Kind,
84        message: String,
85        cause: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
86    ) -> Self {
87        Error {
88            kind,
89            message,
90            cause,
91            rich: None,
92        }
93    }
94
95    /// 新建一条没有 cause 的分类错误。
96    pub fn new(kind: Kind, msg: impl Into<String>) -> Self {
97        Error::base(kind, msg.into(), None)
98    }
99
100    /// 用格式串新建一条分类错误。
101    pub fn errorf(kind: Kind, format: std::fmt::Arguments<'_>) -> Self {
102        Error::base(kind, fmt::format(format), None)
103    }
104
105    /// 把任意错误包成分类错误:消息取 cause 的消息。
106    pub fn wrap(kind: Kind, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
107        Error::base(kind, String::new(), Some(Box::new(cause)))
108    }
109
110    /// 既有消息又有 cause 的分类错误。
111    pub fn wrap_msg(
112        kind: Kind,
113        cause: impl std::error::Error + Send + Sync + 'static,
114        msg: impl Into<String>,
115    ) -> Self {
116        Error::base(kind, msg.into(), Some(Box::new(cause)))
117    }
118
119    /// 把任意用户错误升级为库错误:只包一层(返回 Box 进 source 链),
120    /// classify 会沿链找用户内层已有的分类。用户错误本身不丢。
121    pub fn upgrade(cause: impl std::error::Error + Send + Sync + 'static) -> Self {
122        Error::base(Kind::Internal, String::new(), Some(Box::new(cause)))
123    }
124
125    fn rich_mut(&mut self) -> &mut Rich {
126        self.rich.get_or_insert_with(|| Box::new(Rich::default()))
127    }
128
129    /// §8.5 code:自由域标识,链式挂载(不改变 Kind 映射)。
130    pub fn with_code(mut self, code: impl Into<String>) -> Self {
131        self.rich_mut().code = Some(code.into());
132        self
133    }
134
135    /// §8.5 detail:单条结构化上下文,链式挂载。
136    pub fn with_detail(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
137        self.rich_mut()
138            .detail
139            .get_or_insert_with(serde_json::Map::new)
140            .insert(key.into(), value);
141        self
142    }
143
144    /// §8.5 detail:批量合并结构化上下文,链式挂载。
145    pub fn with_details(mut self, details: serde_json::Map<String, serde_json::Value>) -> Self {
146        self.rich_mut()
147            .detail
148            .get_or_insert_with(serde_json::Map::new)
149            .extend(details);
150        self
151    }
152
153    /// §8.5 status:覆盖 Kind 推导的 HTTP 状态(仅 HTTP 通道生效)。
154    pub fn with_status(mut self, status: u16) -> Self {
155        self.rich_mut().status = Some(status);
156        self
157    }
158
159    /// 从错误链继承富化层(code/detail/status 各取首个非空)——管线包装
160    /// 壳用:handler 已是富化 errors::Error 时,包装不丢 §8.5 语义。
161    pub fn inherit_rich(self, src: &(dyn std::error::Error + 'static)) -> Self {
162        self.inherit_values(rich_fields(src))
163    }
164
165    /// inherit_rich 的取值版本(管线里 cause 已 move 时用)。
166    pub(crate) fn inherit_values(self, (code, detail, status): RichValues) -> Self {
167        if code.is_none() && detail.is_none() && status.is_none() {
168            return self;
169        }
170        let mut me = self;
171        let rich = me.rich_mut();
172        if rich.code.is_none() {
173            rich.code = code;
174        }
175        if rich.detail.is_none() {
176            rich.detail = detail;
177        }
178        if rich.status.is_none() {
179            rich.status = status;
180        }
181        me
182    }
183
184    pub fn kind(&self) -> Kind {
185        self.kind
186    }
187
188    /// §8.5 HTTP 状态覆盖(None = 按 Kind 推导)。
189    pub fn status_override(&self) -> Option<u16> {
190        self.rich.as_ref().and_then(|r| r.status)
191    }
192
193    /// 解开一层到最内层已知 cause(Go 的 Cause)。
194    pub fn cause(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
195        self.cause.as_deref()
196    }
197}
198
199impl fmt::Display for Error {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        match (&self.message.is_empty(), &self.cause) {
202            (false, Some(cause)) => write!(f, "{}: {}", self.message, cause),
203            (false, None) => f.write_str(&self.message),
204            (true, Some(cause)) => write!(f, "{}", cause),
205            (true, None) => f.write_str(self.kind.as_str()),
206        }
207    }
208}
209
210impl std::error::Error for Error {
211    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
212        self.cause
213            .as_ref()
214            .map(|c| c.as_ref() as &(dyn std::error::Error + 'static))
215    }
216}
217
218impl From<std::io::Error> for Error {
219    fn from(e: std::io::Error) -> Self {
220        // 保留底层 io::Error 进 cause 链:显示文本与历史形态完全一致
221        //("io: …"),同时让 EPIPE 之类的类型信息可被沿链检测。
222        Error::wrap_msg(Kind::Internal, e, "io")
223    }
224}
225
226/// 错误链上是否为管道早关(EPIPE):CLI 出口据此静默终止(对齐 Go
227/// 进程被 SIGPIPE 终止的 128+13=141 行为),而不是打印错误体。
228pub fn is_broken_pipe(err: &(dyn std::error::Error + 'static)) -> bool {
229    let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
230    while let Some(e) = cur {
231        if let Some(io) = e.downcast_ref::<std::io::Error>()
232            && io.kind() == std::io::ErrorKind::BrokenPipe
233        {
234            return true;
235        }
236        cur = e.source();
237    }
238    false
239}
240
241/// 沿错误链找到第一条带分类的错误并返回其 Kind:
242/// 未分类的非空错误兜底 Internal。入参需 'static 具体错误(或经
243/// source() 拿到的 &dyn Error + 'static)。
244pub fn classify(err: &(dyn std::error::Error + 'static)) -> Option<Kind> {
245    let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
246    while let Some(e) = cur {
247        if let Some(ce) = e.downcast_ref::<Error>() {
248            return Some(ce.kind);
249        }
250        // 用户错误链里可能嵌套我们自己的分类错误(例如 io::Error 包了 Error)。
251        cur = e.source();
252    }
253    Some(Kind::Internal)
254}
255
256/// 把 Kind 翻译成自然的 HTTP 状态码。
257pub fn http_status(kind: Kind) -> u16 {
258    match kind {
259        Kind::InvalidInput => 400,
260        Kind::Unauthorized => 401,
261        Kind::Forbidden => 403,
262        Kind::NotFound => 404,
263        Kind::Conflict => 409,
264        Kind::Unavailable => 503,
265        Kind::Canceled => 499, // 非标准但广为人知;前端可自行覆盖
266        Kind::Internal => 500,
267    }
268}
269
270/// 把 Kind 翻译成 CLI 进程退出码。
271pub fn exit_code(kind: Kind) -> i32 {
272    if kind == Kind::InvalidInput {
273        2 // 与常规 flag 解析失败同码
274    } else {
275        1
276    }
277}
278
279/// 把 Kind 翻译成 JSON-RPC 2.0 错误码。-32000..-32099 是 JSON-RPC 与 MCP
280/// 保留给应用自定义服务端错误的区间。
281pub fn jsonrpc_code(kind: Kind) -> i64 {
282    match kind {
283        Kind::InvalidInput => -32602, // Invalid params
284        Kind::NotFound => -32001,
285        Kind::Conflict => -32009,
286        Kind::Unauthorized => -32010,
287        Kind::Forbidden => -32011,
288        Kind::Canceled => -32012,
289        _ => -32603, // Internal error
290    }
291}
292
293/// 命令 handler 的 canonical 返回类型。
294pub type Result<T> = std::result::Result<T, Error>;
295
296/// 沿错误链找第一个分类载体(Go errors.From 的对应物)。
297pub fn from_chain<'a>(err: &'a (dyn std::error::Error + 'static)) -> Option<&'a Error> {
298    let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
299    while let Some(e) = cur {
300        if let Some(coded) = e.downcast_ref::<Error>() {
301            return Some(coded);
302        }
303        cur = e.source();
304    }
305    None
306}
307
308/// 沿链合并富化字段:各字段取首个非空(包装壳不遮蔽内层 handler 错误)。
309pub(crate) type RichValues = (
310    Option<String>,
311    Option<serde_json::Map<String, serde_json::Value>>,
312    Option<u16>,
313);
314
315pub(crate) fn rich_fields(err: &(dyn std::error::Error + 'static)) -> RichValues {
316    let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
317    let (mut code, mut detail, mut status) = (None, None, None);
318    while let Some(e) = cur {
319        if let Some(ce) = e.downcast_ref::<Error>()
320            && let Some(r) = &ce.rich
321        {
322            if code.is_none() {
323                code = r.code.clone();
324            }
325            if detail.is_none() {
326                detail = r.detail.clone();
327            }
328            if status.is_none() {
329                status = r.status;
330            }
331        }
332        cur = e.source();
333    }
334    (code, detail, status)
335}
336
337/// 最内层 cause 的消息(spec §8.4 的 most-specific 语义;无 cause 即自身)。
338fn deepest_message(err: &(dyn std::error::Error + 'static)) -> String {
339    let mut cur: &(dyn std::error::Error + 'static) = err;
340    while let Some(src) = cur.source() {
341        cur = src;
342    }
343    cur.to_string()
344}
345
346/// §8.6 共享错误体:HTTP 响应体、CLI 机器格式 stderr 与 MCP `_meta.xyz.error`
347/// 共用的单一形状。平铺 `error` 键与旧契约字节兼容,`code`/`detail` 纯增量。
348#[derive(Debug, Clone, serde::Serialize)]
349pub struct Body {
350    pub error: String,
351    pub kind: Kind,
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub code: Option<String>,
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub detail: Option<serde_json::Map<String, serde_json::Value>>,
356}
357
358impl Default for Body {
359    fn default() -> Self {
360        Body {
361            error: String::new(),
362            kind: Kind::Internal,
363            code: None,
364            detail: None,
365        }
366    }
367}
368
369/// 把任意错误投射为共享错误体(spec §8.6)。消息取最内层 cause,
370/// 富化层(code/detail)从链上第一个分类载体上取。
371pub fn error_body(err: &(dyn std::error::Error + 'static)) -> Body {
372    let mut b = Body {
373        error: deepest_message(err),
374        kind: classify(err).unwrap_or(Kind::Internal),
375        code: None,
376        detail: None,
377    };
378    let (code, detail, _) = rich_fields(err);
379    b.code = code;
380    b.detail = detail;
381    b
382}
383
384/// HTTP 状态:显式 §8.5 status 覆盖优先,否则按 Kind 推导。
385pub fn error_status(err: &(dyn std::error::Error + 'static)) -> u16 {
386    let (_, _, status) = rich_fields(err);
387    if let Some(s) = status {
388        return s;
389    }
390    http_status(classify(err).unwrap_or(Kind::Internal))
391}
392
393/// 便捷构造函数(对齐 Go 的 errs.New/errs.Errorf/errs.Wrap)。
394pub fn new(kind: Kind, msg: impl Into<String>) -> Error {
395    Error::new(kind, msg)
396}
397
398/// 返回带格式化消息的分类错误:
399/// `errs::errorf!(errs::Kind::NotFound, "user {} not found", name)`
400#[macro_export]
401macro_rules! errorf {
402    ($kind:expr, $($arg:tt)*) => {
403        $crate::errors::Error::errorf($kind, format_args!($($arg)*))
404    };
405}
406
407pub use errorf;
408#[cfg(test)]
409mod tests {
410    use super::*;
411
412    #[test]
413    fn mappings() {
414        assert_eq!(http_status(Kind::InvalidInput), 400);
415        assert_eq!(http_status(Kind::Unauthorized), 401);
416        assert_eq!(http_status(Kind::Forbidden), 403);
417        assert_eq!(http_status(Kind::NotFound), 404);
418        assert_eq!(http_status(Kind::Conflict), 409);
419        assert_eq!(http_status(Kind::Unavailable), 503);
420        assert_eq!(http_status(Kind::Canceled), 499);
421        assert_eq!(http_status(Kind::Internal), 500);
422
423        assert_eq!(exit_code(Kind::InvalidInput), 2);
424        assert_eq!(exit_code(Kind::NotFound), 1);
425
426        assert_eq!(jsonrpc_code(Kind::InvalidInput), -32602);
427        assert_eq!(jsonrpc_code(Kind::NotFound), -32001);
428        assert_eq!(jsonrpc_code(Kind::Conflict), -32009);
429        assert_eq!(jsonrpc_code(Kind::Unauthorized), -32010);
430        assert_eq!(jsonrpc_code(Kind::Forbidden), -32011);
431        assert_eq!(jsonrpc_code(Kind::Canceled), -32012);
432        assert_eq!(jsonrpc_code(Kind::Internal), -32603);
433    }
434
435    #[test]
436    fn classify_chain() {
437        let e = Error::wrap_msg(Kind::NotFound, Error::new(Kind::Conflict, "inner"), "outer");
438        assert_eq!(classify(&e), Some(Kind::NotFound));
439        // 内层 cause 类型也保留在链上(Cause 语义;Error::source() 在
440        // std::error::Error 上,给出 &dyn Error + 'static)。
441        let cause = std::error::Error::source(&e).unwrap();
442        assert_eq!(classify(cause), Some(Kind::Conflict));
443    }
444
445    #[test]
446    fn error_display_forms() {
447        assert_eq!(Error::new(Kind::NotFound, "x").to_string(), "x");
448        let wrapped = Error::wrap(Kind::InvalidInput, std::io::Error::other("ioerr"));
449        assert_eq!(wrapped.to_string(), "ioerr");
450        let both = Error::wrap_msg(Kind::Internal, std::io::Error::other("ioerr"), "boom");
451        assert_eq!(both.to_string(), "boom: ioerr");
452    }
453}