Skip to main content

sz_rust_addons_loader/
error.rs

1//! 插件加载器错误类型
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `think\addons` 抛出的 `HttpException`:
6//!
7//! | PHP 异常 | Rust 错误 | 说明 |
8//! |----------|----------|------|
9//! | `HttpException(500, 'addon can not be empty')` | `AddonNotFound` | 插件名缺失 |
10//! | `HttpException(404, 'addon %s not found')` | `AddonNotFound` | 插件不存在 |
11//! | `HttpException(500, 'addon %s is disabled')` | `AddonDisabled` | 插件已禁用 |
12//! | `HttpException(404, 'addon controller %s not found')` | `ControllerNotFound` | 控制器不存在 |
13//! | `HttpException(404, 'addon action %s not found')` | `ActionNotFound` | 操作不存在 |
14
15use thiserror::Error;
16
17/// 插件加载器错误
18#[derive(Debug, Error)]
19pub enum AddonLoaderError {
20    /// 插件不存在(对齐 PHP `HttpException(404, 'addon %s not found')`)
21    #[error("addon '{0}' not found")]
22    AddonNotFound(String),
23
24    /// 插件已禁用(对齐 PHP `HttpException(500, 'addon %s is disabled')`)
25    #[error("addon '{0}' is disabled")]
26    AddonDisabled(String),
27
28    /// 控制器不存在(对齐 PHP `HttpException(404, 'addon controller %s not found')`)
29    #[error("addon controller '{0}' not found")]
30    ControllerNotFound(String),
31
32    /// 操作不存在(对齐 PHP `HttpException(404, 'addon action %s not found')`)
33    #[error("addon action '{0}' not found")]
34    ActionNotFound(String),
35
36    /// 插件清单解析失败(Plugin.php 中 `$info` 数组格式错误)
37    #[error("failed to parse manifest for addon '{addon}': {reason}")]
38    ManifestParse {
39        /// 插件名
40        addon: String,
41        /// 失败原因
42        reason: String,
43    },
44
45    /// 插件目录扫描失败(IO 错误)
46    #[error("failed to scan addons directory '{path}': {source}")]
47    ScanDir {
48        /// 目录路径
49        path: String,
50        /// 底层 IO 错误
51        source: std::io::Error,
52    },
53
54    /// 文件读取失败
55    #[error("failed to read file '{path}': {source}")]
56    ReadFile {
57        /// 文件路径
58        path: String,
59        /// 底层 IO 错误
60        source: std::io::Error,
61    },
62
63    /// 自动加载类映射失败(对齐 PHP `spl_autoload_register` 找不到文件时返回 false)
64    #[error("autoload failed: class '{class}' not mapped to any file")]
65    AutoloadMiss {
66        /// 类名(如 `addons\operate\Plugin`)
67        class: String,
68    },
69
70    /// 钩子注册失败
71    #[error("failed to register hook '{hook}' for addon '{addon}'")]
72    HookRegister {
73        /// 钩子名
74        hook: String,
75        /// 插件名
76        addon: String,
77    },
78
79    /// 路由解析失败
80    #[error("failed to parse route '{url}': {reason}")]
81    RouteParse {
82        /// URL
83        url: String,
84        /// 失败原因
85        reason: String,
86    },
87}
88
89impl From<std::io::Error> for AddonLoaderError {
90    fn from(err: std::io::Error) -> Self {
91        AddonLoaderError::ReadFile {
92            path: "<unknown>".to_string(),
93            source: err,
94        }
95    }
96}
97
98/// 插件加载器 Result 别名
99pub type AddonLoaderResult<T> = Result<T, AddonLoaderError>;
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_addon_not_found_display() {
107        let err = AddonLoaderError::AddonNotFound("operate".to_string());
108        assert_eq!(err.to_string(), "addon 'operate' not found");
109    }
110
111    #[test]
112    fn test_addon_disabled_display() {
113        let err = AddonLoaderError::AddonDisabled("test".to_string());
114        assert_eq!(err.to_string(), "addon 'test' is disabled");
115    }
116
117    #[test]
118    fn test_controller_not_found_display() {
119        let err = AddonLoaderError::ControllerNotFound("admin.Order".to_string());
120        assert_eq!(err.to_string(), "addon controller 'admin.Order' not found");
121    }
122
123    #[test]
124    fn test_action_not_found_display() {
125        let err = AddonLoaderError::ActionNotFound("index".to_string());
126        assert_eq!(err.to_string(), "addon action 'index' not found");
127    }
128
129    #[test]
130    fn test_manifest_parse_display() {
131        let err = AddonLoaderError::ManifestParse {
132            addon: "operate".to_string(),
133            reason: "missing $info array".to_string(),
134        };
135        assert!(err.to_string().contains("operate"));
136        assert!(err.to_string().contains("missing $info array"));
137    }
138
139    #[test]
140    fn test_scan_dir_display() {
141        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such directory");
142        let err = AddonLoaderError::ScanDir {
143            path: "/addons".to_string(),
144            source: io_err,
145        };
146        assert!(err.to_string().contains("/addons"));
147    }
148
149    #[test]
150    fn test_read_file_display() {
151        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
152        let err = AddonLoaderError::ReadFile {
153            path: "/addons/operate/Plugin.php".to_string(),
154            source: io_err,
155        };
156        assert!(err.to_string().contains("Plugin.php"));
157    }
158
159    #[test]
160    fn test_autoload_miss_display() {
161        let err = AddonLoaderError::AutoloadMiss {
162            class: "addons\\operate\\Plugin".to_string(),
163        };
164        assert!(err.to_string().contains("addons\\operate\\Plugin"));
165    }
166
167    #[test]
168    fn test_hook_register_display() {
169        let err = AddonLoaderError::HookRegister {
170            hook: "AddonsInit".to_string(),
171            addon: "operate".to_string(),
172        };
173        assert!(err.to_string().contains("AddonsInit"));
174        assert!(err.to_string().contains("operate"));
175    }
176
177    #[test]
178    fn test_route_parse_display() {
179        let err = AddonLoaderError::RouteParse {
180            url: "/addons/operate".to_string(),
181            reason: "missing controller".to_string(),
182        };
183        assert!(err.to_string().contains("/addons/operate"));
184    }
185
186    #[test]
187    fn test_from_io_error() {
188        let io_err = std::io::Error::other("test");
189        let err: AddonLoaderError = io_err.into();
190        assert!(matches!(err, AddonLoaderError::ReadFile { .. }));
191    }
192
193    #[test]
194    fn test_result_alias_ok() {
195        let result: AddonLoaderResult<i32> = Ok(42);
196        match result {
197            Ok(v) => assert_eq!(v, 42),
198            Err(_) => panic!("expected Ok"),
199        }
200    }
201
202    #[test]
203    fn test_result_alias_err() {
204        let result: AddonLoaderResult<i32> = Err(AddonLoaderError::AddonNotFound("x".to_string()));
205        assert!(result.is_err());
206    }
207}