Skip to main content

sz_rust_cli/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! CLI 错误类型
5//!
6//! 对齐 PHP `think\console\Command` 的错误处理模式:
7//! - PHP 通过返回 `false` 或抛出异常
8//! - Rust 使用 `Result<T, CliError>` 统一错误处理
9
10use std::path::PathBuf;
11use thiserror::Error;
12
13/// CLI 错误
14#[derive(Debug, Error)]
15pub enum CliError {
16    /// 文件 IO 错误(创建文件、读取模板等)
17    #[error("IO error: {0}")]
18    Io(#[from] std::io::Error),
19
20    /// 文件已存在(对齐 PHP `Make::execute` 中 `already exists!` 提示)
21    #[error("File already exists: {0}")]
22    FileExists(String),
23
24    /// clap 参数解析错误(对齐 PHP `console\Input` 验证失败)
25    #[error("Clap error: {0}")]
26    Clap(String),
27
28    /// 代码生成错误(模板替换失败等)
29    #[error("Generation error: {0}")]
30    Generation(String),
31
32    /// 数据库迁移错误
33    #[error("Migration error: {0}")]
34    Migration(String),
35
36    /// 缓存清理错误
37    #[error("Cache error: {0}")]
38    Cache(String),
39
40    /// 调度器错误
41    #[error("Scheduler error: {0}")]
42    Scheduler(String),
43
44    /// 通用错误
45    #[error("{0}")]
46    Generic(String),
47
48    /// P1-T3: 非法插件名称(不符合 Rust crate 命名规范)
49    #[error("Invalid plugin name: {0} (must be lowercase letters, digits, underscores or hyphens, not starting with a digit)")]
50    InvalidPluginName(String),
51
52    /// P1-T3: 未知模板类型(附带用户请求的模板名与可用模板列表)
53    #[error("Unknown template: '{requested}'. Available templates: {available:?}")]
54    UnknownTemplate {
55        /// 用户请求的模板类型名
56        requested: String,
57        /// 可用模板类型列表
58        available: Vec<String>,
59    },
60
61    /// P1-T3: 字段定义解析错误
62    #[error("Field parse error: {0}")]
63    FieldParseError(String),
64
65    /// P1-T3: 目标目录已存在(需 --force 覆盖)
66    #[error("Directory already exists: {0} (use --force to overwrite)")]
67    DirExists(PathBuf),
68
69    /// P1-T3: 模板文件缺失(附带缺失文件列表)
70    #[error("Template files missing: {0:?}")]
71    TemplateMissing(Vec<String>),
72
73    /// P1-T3: 模板语法错误(含文件名/行号/列号/错误消息)
74    #[error("Template syntax error in {file}:{line}:{col}: {msg}")]
75    TemplateSyntaxError {
76        /// 模板文件名
77        file: String,
78        /// 行号(1-based)
79        line: usize,
80        /// 列号(1-based)
81        col: usize,
82        /// 错误消息
83        msg: String,
84    },
85
86    /// P1-T3: 模板变量未找到(含变量名/引用文件/行号)
87    #[error("Variable not found: '{var}' referenced in {file}:{line}")]
88    VarNotFound {
89        /// 缺失的变量名
90        var: String,
91        /// 引用该变量的模板文件名
92        file: String,
93        /// 行号(1-based)
94        line: usize,
95    },
96
97    /// P1-T3: cargo check 编译失败(含编译错误列表)
98    #[error("Compilation failed: {0:?}")]
99    CompileFailed(Vec<String>),
100
101    /// P1-T3: 外键字段不存在于从表字段定义中
102    #[error("Foreign key not found: '{0}' is not a field in the slave table")]
103    ForeignKeyNotFound(String),
104
105    /// P1-T3: 主表与从表同名
106    #[error("Master table and slave table must be different")]
107    MasterSlaveSame,
108
109    /// P2-2: 插件市场错误
110    #[error("Marketplace error: {0}")]
111    Marketplace(String),
112
113    /// P2-2: HTTP 请求错误
114    #[error("HTTP error: {0}")]
115    Http(String),
116
117    /// P2-2: TOML 序列化错误
118    #[error("TOML error: {0}")]
119    Toml(String),
120
121    /// P2-2: JSON 序列化错误
122    #[error("JSON error: {0}")]
123    Json(String),
124}
125
126impl From<clap::Error> for CliError {
127    fn from(e: clap::Error) -> Self {
128        CliError::Clap(e.to_string())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_io_error_display() {
138        let err = CliError::Io(std::io::Error::new(
139            std::io::ErrorKind::NotFound,
140            "file not found",
141        ));
142        assert!(err.to_string().contains("IO error"));
143        assert!(err.to_string().contains("file not found"));
144    }
145
146    #[test]
147    fn test_file_exists_error_display() {
148        let err = CliError::FileExists("/path/to/file".to_string());
149        assert!(err.to_string().contains("File already exists"));
150        assert!(err.to_string().contains("/path/to/file"));
151    }
152
153    #[test]
154    fn test_clap_error_conversion() {
155        let clap_err = clap::Error::new(clap::error::ErrorKind::InvalidValue);
156        let cli_err: CliError = clap_err.into();
157        assert!(matches!(cli_err, CliError::Clap(_)));
158    }
159
160    #[test]
161    fn test_generation_error_display() {
162        let err = CliError::Generation("template substitution failed".to_string());
163        assert!(err.to_string().contains("Generation error"));
164    }
165
166    #[test]
167    fn test_migration_error_display() {
168        let err = CliError::Migration("database connection failed".to_string());
169        assert!(err.to_string().contains("Migration error"));
170    }
171
172    #[test]
173    fn test_cache_error_display() {
174        let err = CliError::Cache("redis connection failed".to_string());
175        assert!(err.to_string().contains("Cache error"));
176    }
177
178    #[test]
179    fn test_scheduler_error_display() {
180        let err = CliError::Scheduler("cron parse failed".to_string());
181        assert!(err.to_string().contains("Scheduler error"));
182    }
183
184    #[test]
185    fn test_invalid_plugin_name_display() {
186        let err = CliError::InvalidPluginName("my plugin".to_string());
187        assert!(err.to_string().contains("Invalid plugin name"));
188        assert!(err.to_string().contains("my plugin"));
189    }
190
191    #[test]
192    fn test_unknown_template_display() {
193        let err = CliError::UnknownTemplate {
194            requested: "nonexistent".to_string(),
195            available: vec!["crud".to_string(), "master-slave".to_string()],
196        };
197        assert!(err.to_string().contains("Unknown template"));
198        assert!(err.to_string().contains("nonexistent"));
199        assert!(err.to_string().contains("crud"));
200    }
201
202    #[test]
203    fn test_field_parse_error_display() {
204        let err = CliError::FieldParseError("unexpected ',' at position 5".to_string());
205        assert!(err.to_string().contains("Field parse error"));
206    }
207
208    #[test]
209    fn test_dir_exists_display() {
210        let err = CliError::DirExists(PathBuf::from("/path/to/plugin"));
211        assert!(err.to_string().contains("Directory already exists"));
212        assert!(err.to_string().contains("--force"));
213    }
214
215    #[test]
216    fn test_template_missing_display() {
217        let err = CliError::TemplateMissing(vec!["model.rs.tera".to_string()]);
218        assert!(err.to_string().contains("Template files missing"));
219        assert!(err.to_string().contains("model.rs.tera"));
220    }
221
222    #[test]
223    fn test_template_syntax_error_display() {
224        let err = CliError::TemplateSyntaxError {
225            file: "model.rs.tera".to_string(),
226            line: 10,
227            col: 5,
228            msg: "unexpected token".to_string(),
229        };
230        let s = err.to_string();
231        assert!(s.contains("model.rs.tera"));
232        assert!(s.contains("10"));
233        assert!(s.contains("5"));
234        assert!(s.contains("unexpected token"));
235    }
236
237    #[test]
238    fn test_var_not_found_display() {
239        let err = CliError::VarNotFound {
240            var: "plugin_name".to_string(),
241            file: "model.rs.tera".to_string(),
242            line: 3,
243        };
244        let s = err.to_string();
245        assert!(s.contains("plugin_name"));
246        assert!(s.contains("model.rs.tera"));
247    }
248
249    #[test]
250    fn test_compile_failed_display() {
251        let err =
252            CliError::CompileFailed(vec!["error[E0277]: trait bound not satisfied".to_string()]);
253        assert!(err.to_string().contains("Compilation failed"));
254    }
255
256    #[test]
257    fn test_foreign_key_not_found_display() {
258        let err = CliError::ForeignKeyNotFound("user_id".to_string());
259        assert!(err.to_string().contains("Foreign key not found"));
260        assert!(err.to_string().contains("user_id"));
261    }
262
263    #[test]
264    fn test_master_slave_same_display() {
265        let err = CliError::MasterSlaveSame;
266        assert!(err
267            .to_string()
268            .contains("Master table and slave table must be different"));
269    }
270}