//! 错误形状与退出码。 //! //! 约定(10-cli-design.md §3):**0 成功 / 1 用户错误 / 2 内部错误**。 //! 用户错误 = 用法错、目标不存在、环境不支持、依赖模块未接线; //! 内部错误 = 不该发生的事(序列化失配等),脚本见到 2 应该当 bug 报。 use std::fmt; use serde_json::{json, Value}; /// 退出码:成功。 pub const EXIT_OK: i32 = 0; /// 退出码:用户错误。 pub const EXIT_USER: i32 = 1; /// 退出码:内部错误。 pub const EXIT_INTERNAL: i32 = 2; /// 错误大类(决定退出码)。 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorKind { /// 用户侧问题:改参数/改环境可解决。 User, /// 内部问题:不该发生,应报 bug。 Internal, } impl ErrorKind { /// 稳定名(`--json` 输出用)。 #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::User => "user", Self::Internal => "internal", } } /// 对应的进程退出码。 #[must_use] pub const fn exit_code(self) -> i32 { match self { Self::User => EXIT_USER, Self::Internal => EXIT_INTERNAL, } } } /// 一条 CLI 错误。 /// /// `code` 是给脚本的稳定机读标识(`usage` / `not_found` / `unsupported_os` …), /// `message` 是给人看的中文说明,`hint` 是「下一步该做什么」。 #[derive(Debug, Clone)] pub struct CliError { kind: ErrorKind, code: &'static str, message: String, hint: Option, } impl CliError { /// 用法错误(参数不对)。 #[must_use] pub fn usage(message: impl Into) -> Self { Self { kind: ErrorKind::User, code: "usage", message: message.into(), hint: None, } } /// 用户错误,带稳定 code。 #[must_use] pub fn user(code: &'static str, message: impl Into) -> Self { Self { kind: ErrorKind::User, code, message: message.into(), hint: None, } } /// 内部错误,带稳定 code。 #[must_use] pub fn internal(code: &'static str, message: impl Into) -> Self { Self { kind: ErrorKind::Internal, code, message: message.into(), hint: None, } } /// IO 失败(读配置/读写会话文件):按用户错误处理,路径进消息。 #[must_use] pub fn io(op: &str, path: &std::path::Path, error: &std::io::Error) -> Self { Self::user("io", format!("{op}失败:{}({error})", path.display())) } /// 补一句提示。 #[must_use] pub fn with_hint(mut self, hint: impl Into) -> Self { self.hint = Some(hint.into()); self } /// 错误大类。 #[must_use] pub const fn kind(&self) -> ErrorKind { self.kind } /// 稳定错误码。 #[must_use] pub const fn code(&self) -> &'static str { self.code } /// 人类可读说明。 #[must_use] pub fn message(&self) -> &str { &self.message } /// 下一步提示。 #[must_use] pub fn hint(&self) -> Option<&str> { self.hint.as_deref() } /// 进程退出码。 #[must_use] pub const fn exit_code(&self) -> i32 { self.kind.exit_code() } /// `--json` 里的错误对象。 #[must_use] pub fn to_json(&self) -> Value { let mut error = json!({ "code": self.code, "kind": self.kind.as_str(), "message": self.message, }); if let Some(hint) = &self.hint { error["hint"] = Value::String(hint.clone()); } json!({ "ok": false, "error": error }) } } impl fmt::Display for CliError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!(formatter, "{}", self.message) } } impl std::error::Error for CliError {}