Skip to main content

evorule_cli/
error.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! CLI 错误类型
5//!
6//! 统一的错误枚举,覆盖 I/O、JSON、规则加载、payload 解析、fact log 解析、
7//! TCB 执行、WAL 序列化等场景。所有子命令返回 `Result<(), CliError>`,
8//! main.rs 根据错误类型决定退出码。
9
10use thiserror::Error;
11
12/// CLI 错误
13///
14/// 设计原则:
15/// - 每个变体携带足够上下文(路径、行号、原因),便于用户定位问题
16/// - `#[from]` 自动转换 std::io::Error / serde_json::Error / WalError
17/// - Display 输出人类可读的中文/英文混合消息(与现有 CLI 风格一致)
18#[derive(Debug, Error)]
19pub enum CliError {
20    /// 文件系统 I/O 错误
21    #[error("I/O error: {0}")]
22    Io(#[from] std::io::Error),
23
24    /// JSON 序列化/反序列化错误
25    #[error("JSON error: {0}")]
26    Json(#[from] serde_json::Error),
27
28    /// tier1 WAL 序列化错误(fact_from_json 失败等)
29    #[error("WAL error: {0}")]
30    Wal(#[from] evorule_reactor::WalError),
31
32    /// 规则目录不存在
33    #[error("Rules directory does not exist: {0}")]
34    RulesDirNotFound(String),
35
36    /// 规则目录中无 .json 文件
37    #[error("No .json files found in {0}")]
38    NoRulesFound(String),
39
40    /// 初始 payload JSON 解析失败
41    #[error("Invalid payload JSON: {0}")]
42    InvalidPayload(String),
43
44    /// fact log 解析错误(指定行号和原因)
45    #[error("Fact log parse error at line {line}: {reason}")]
46    FactLogParse {
47        /// 出错的行号(1-based)
48        line: usize,
49        /// 错误原因
50        reason: String,
51    },
52
53    /// 哈希链验证失败
54    #[error("Hash chain verification failed: {0}")]
55    HashChain(String),
56
57    /// 通用错误(兜底)
58    #[error("{0}")]
59    Other(String),
60}
61
62impl CliError {
63    /// 从任意字符串创建通用错误
64    ///
65    /// # 示例
66    /// ```
67    /// use evorule_cli::CliError;
68    ///
69    /// let err = CliError::other("something went wrong");
70    /// assert_eq!(err.to_string(), "something went wrong");
71    /// ```
72    pub fn other(msg: impl Into<String>) -> Self {
73        Self::Other(msg.into())
74    }
75}
76
77/// 退出码映射
78///
79/// 约定:
80/// - 0:成功
81/// - 1:通用错误(默认)
82/// - 2:规则加载错误(目录不存在、无 .json)
83impl CliError {
84    /// 返回该错误对应的退出码
85    ///
86    /// # 约定
87    /// - 0:成功
88    /// - 1:通用错误(默认)
89    /// - 2:规则加载错误(目录不存在、无 .json)
90    ///
91    /// # 示例
92    /// ```
93    /// use evorule_cli::CliError;
94    ///
95    /// // 规则目录缺失 → 退出码 2
96    /// let dir_err = CliError::RulesDirNotFound("/nonexistent".into());
97    /// assert_eq!(dir_err.exit_code(), 2);
98    ///
99    /// // 通用错误 → 退出码 1
100    /// assert_eq!(CliError::other("boom").exit_code(), 1);
101    /// ```
102    pub fn exit_code(&self) -> i32 {
103        match self {
104            CliError::RulesDirNotFound(_) | CliError::NoRulesFound(_) => 2,
105            _ => 1,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    #![allow(clippy::unwrap_used)]
113    use super::*;
114
115    #[test]
116    fn test_exit_code_mapping() {
117        assert_eq!(CliError::RulesDirNotFound("x".into()).exit_code(), 2);
118        assert_eq!(CliError::NoRulesFound("x".into()).exit_code(), 2);
119        assert_eq!(CliError::Other("x".into()).exit_code(), 1);
120        assert_eq!(
121            CliError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "x")).exit_code(),
122            1
123        );
124    }
125
126    #[test]
127    fn test_from_io_error() {
128        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
129        let cli_err: CliError = io_err.into();
130        assert!(matches!(cli_err, CliError::Io(_)));
131    }
132
133    #[test]
134    fn test_display_includes_context() {
135        let err = CliError::FactLogParse {
136            line: 42,
137            reason: "missing type field".into(),
138        };
139        let msg = format!("{}", err);
140        assert!(msg.contains("42"));
141        assert!(msg.contains("missing type field"));
142    }
143}