Skip to main content

sz_rust_addons_loader/
lib.rs

1//! # SZ-Rust Addons Loader — 插件加载器
2//!
3//! 对标 PHP `zzstudio/think-addons` 的 Rust 实现,提供插件发现/清单解析/注册/路由解析能力。
4//!
5//! ## PHP 对齐
6//!
7//! | PHP 模块 | Rust 模块 | 子任务 |
8//! |----------|----------|--------|
9//! | `think\addons\Service` | `loader::AddonLoader` | 10.1 |
10//! | `think\Addons::getInfo()` | `manifest::parse_manifest` | 10.1 |
11//! | `spl_autoload_register`(helper.php) | `autoload::AddonAutoload` | 10.1 |
12//! | 隐式插件状态管理 | `registry::AddonRegistry` | 10.2 |
13//! | `think\addons\Route::execute` | `route::parse_route` | 10.3 |
14//! | `think\Route::resource` | `resource::build_resource_routes` | 10.3.5 |
15//!
16//! ## 模块结构
17//!
18//! ```text
19//! sz-rust-addons-loader/
20//! ├── Cargo.toml
21//! └── src/
22//!     ├── lib.rs          # 主入口(重导出)
23//!     ├── error.rs        # 错误类型
24//!     ├── loader.rs       # AddonLoader(对齐 Service.php)
25//!     ├── manifest.rs     # 清单解析(对齐 Addons::getInfo)
26//!     ├── autoload.rs     # 自动加载(对齐 spl_autoload_register)
27//!     ├── registry.rs     # 注册中心(对齐插件状态管理)
28//!     ├── route.rs        # 路由解析(对齐 Route::execute)
29//!     └── resource.rs     # 资源路由注入(对齐 Route::resource)
30//! ```
31//!
32//! ## 用法
33//!
34//! ```rust,ignore
35//! use sz_rust_addons_loader::loader::AddonLoader;
36//!
37//! let loader = AddonLoader::new("/path/to/addons");
38//! let errors = loader.register().unwrap();
39//! // errors 包含解析失败的插件错误(不中断整体扫描)
40//!
41//! let route = loader.parse_route("/addons/operate/Order/index").unwrap();
42//! assert_eq!(route.addon, "operate");
43//! assert_eq!(route.controller, "Order");
44//! assert_eq!(route.action, "index");
45//! ```
46//!
47//! ## 不支持(与 PHP 差异)
48//!
49//! - **不实际加载 PHP 文件**:Rust 无法直接执行 PHP,仅解析 `$info` 数组
50//! - **不实现钩子自动扫描**:PHP 通过反射收集 Plugin.php 非基类方法作为钩子,Rust 侧需调用方显式注册
51//! - **不实现 `service.ini` 容器绑定**:Rust 侧无容器概念,由调用方根据需要处理
52//! - **不实现 `_empty` 兜底方法**:对齐 PHP `is_callable([$instance, '_empty'])`,但需调用方实现
53
54#![forbid(unsafe_code)]
55#![warn(missing_docs)]
56
57pub mod autoload;
58pub mod error;
59#[cfg(feature = "hot-reload")]
60pub mod hot_reload;
61pub mod loader;
62pub mod manifest;
63pub mod registry;
64pub mod resource;
65pub mod route;
66
67// 重导出核心类型,对齐 PHP `think\addons` 命名空间
68pub use autoload::AddonAutoload;
69pub use error::{AddonLoaderError, AddonLoaderResult};
70pub use loader::AddonLoader;
71pub use manifest::AddonManifest;
72pub use registry::AddonRegistry;
73pub use resource::{
74    build_resource_routes, HttpMethod, ResourceAction, ResourceOptions, ResourceRoute,
75    ResourceRouteEntry,
76};
77pub use route::AddonRoute;
78
79#[cfg(test)]
80mod integration_tests {
81    use super::*;
82    use std::fs;
83
84    /// 端到端集成测试:完整生命周期
85    ///
86    /// 验证 AddonLoader 的完整流程:
87    /// 1. 创建临时插件目录
88    /// 2. 编写 Plugin.php(含 $info 数组)
89    /// 3. 创建控制器文件
90    /// 4. register() 扫描并注册
91    /// 5. parse_route() 解析 URL
92    /// 6. 验证路由信息正确
93    #[test]
94    fn test_end_to_end_lifecycle() {
95        let tmp = tempfile::tempdir().expect("create tempdir");
96        let addons_path = tmp.path().join("addons");
97
98        // 创建 operate 插件
99        let operate_dir = addons_path.join("operate");
100        fs::create_dir_all(&operate_dir).unwrap();
101        fs::write(
102            operate_dir.join("Plugin.php"),
103            r#"<?php
104namespace addons\operate;
105use think\Addons;
106class Plugin extends Addons {
107    public $info = [
108        'name' => 'operate',
109        'title' => '运营管理',
110        'identifier' => 'operate.addon',
111        'icon' => 'fa-cog',
112        'author' => 'sz',
113        'version' => '1.0.0',
114        'admin' => 'operate/index/index',
115        'status' => 1,
116    ];
117    public function install() {}
118    public function uninstall() {}
119}
120"#,
121        )
122        .unwrap();
123
124        // 创建 operate/controller/Order.php
125        let controller_dir = operate_dir.join("controller");
126        fs::create_dir_all(&controller_dir).unwrap();
127        fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
128
129        // 创建 operate/controller/admin/Order.php
130        let admin_dir = controller_dir.join("admin");
131        fs::create_dir_all(&admin_dir).unwrap();
132        fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
133
134        // 创建 disabled 插件(status=0)
135        let disabled_dir = addons_path.join("disabled");
136        fs::create_dir_all(&disabled_dir).unwrap();
137        fs::write(
138            disabled_dir.join("Plugin.php"),
139            r#"
140public $info = [
141    'name' => 'disabled',
142    'status' => 0,
143];
144"#,
145        )
146        .unwrap();
147
148        // 1. 创建加载器
149        let loader = AddonLoader::new(&addons_path);
150        assert_eq!(loader.count(), 0);
151
152        // 2. 注册所有插件
153        let errors = loader.register().unwrap();
154        assert!(errors.is_empty(), "register errors: {:?}", errors);
155        assert_eq!(loader.count(), 2);
156
157        // 3. 验证 operate 启用、disabled 禁用
158        assert!(loader.is_enabled("operate").unwrap());
159        assert!(!loader.is_enabled("disabled").unwrap());
160
161        // 4. 验证清单字段
162        let manifest = loader.get_manifest("operate").unwrap();
163        assert_eq!(manifest.name, "operate");
164        assert_eq!(manifest.title, "运营管理");
165        assert_eq!(manifest.version, "1.0.0");
166        assert!(manifest.is_enabled());
167
168        // 5. 解析简单路由
169        let route = loader.parse_route("/addons/operate/Order/index").unwrap();
170        assert_eq!(route.addon, "operate");
171        assert_eq!(route.controller, "Order");
172        assert_eq!(route.action, "index");
173        assert_eq!(route.controller_class, "addons\\operate\\controller\\Order");
174        assert!(route.controller_file.is_some());
175        assert!(route.controller_file.unwrap().exists());
176
177        // 6. 解析多级控制器路由(点号分隔)
178        let route2 = loader
179            .parse_route("/addons/operate/admin.Order/index")
180            .unwrap();
181        assert_eq!(route2.controller, "admin.Order");
182        assert_eq!(
183            route2.controller_class,
184            "addons\\operate\\controller\\admin\\Order"
185        );
186        assert!(route2.controller_file.is_some());
187
188        // 7. 禁用插件应抛 AddonDisabled
189        let result = loader.parse_route("/addons/disabled/Order/index");
190        assert!(matches!(result, Err(AddonLoaderError::AddonDisabled(_))));
191
192        // 8. 不存在的插件应抛 AddonNotFound
193        let result = loader.parse_route("/addons/ghost/Order/index");
194        assert!(matches!(result, Err(AddonLoaderError::AddonNotFound(_))));
195    }
196
197    /// 测试混合启用/禁用插件列表
198    #[test]
199    fn test_mixed_enabled_disabled_addons() {
200        let tmp = tempfile::tempdir().unwrap();
201        let addons_path = tmp.path().join("addons");
202
203        for (name, status) in [
204            ("enabled1", 1),
205            ("enabled2", 1),
206            ("disabled1", 0),
207            ("disabled2", 0),
208        ] {
209            let dir = addons_path.join(name);
210            fs::create_dir_all(&dir).unwrap();
211            fs::write(
212                dir.join("Plugin.php"),
213                format!(
214                    r#"
215public $info = [
216    'name' => '{}',
217    'status' => {},
218];
219"#,
220                    name, status
221                ),
222            )
223            .unwrap();
224        }
225
226        let loader = AddonLoader::new(&addons_path);
227        loader.register().unwrap();
228
229        assert_eq!(loader.count(), 4);
230        assert_eq!(loader.enabled_addons().len(), 2);
231        assert_eq!(loader.disabled_addons().len(), 2);
232        assert_eq!(
233            loader.names(),
234            vec!["disabled1", "disabled2", "enabled1", "enabled2"]
235        );
236    }
237
238    /// 测试动态状态切换
239    #[test]
240    fn test_dynamic_status_toggle() {
241        let tmp = tempfile::tempdir().unwrap();
242        let addons_path = tmp.path().join("addons");
243        let dir = addons_path.join("operate");
244        fs::create_dir_all(&dir).unwrap();
245        fs::write(
246            dir.join("Plugin.php"),
247            r#"
248public $info = [
249    'name' => 'operate',
250    'status' => 1,
251];
252"#,
253        )
254        .unwrap();
255
256        let loader = AddonLoader::new(&addons_path);
257        loader.register().unwrap();
258
259        assert!(loader.is_enabled("operate").unwrap());
260
261        // 动态禁用
262        loader.registry().set_enabled("operate", false).unwrap();
263        assert!(!loader.is_enabled("operate").unwrap());
264
265        // 动态启用
266        loader.registry().set_enabled("operate", true).unwrap();
267        assert!(loader.is_enabled("operate").unwrap());
268    }
269
270    /// 测试 autoload 与 registry 协作
271    #[test]
272    fn test_autoload_registry_collaboration() {
273        let tmp = tempfile::tempdir().unwrap();
274        let addons_path = tmp.path().join("addons");
275        let operate_dir = addons_path.join("operate");
276        fs::create_dir_all(&operate_dir).unwrap();
277        fs::write(operate_dir.join("Plugin.php"), "<?php // stub").unwrap();
278        fs::write(
279            operate_dir.join("Plugin.php"),
280            r#"
281public $info = [
282    'name' => 'operate',
283    'status' => 1,
284];
285"#,
286        )
287        .unwrap();
288
289        let controller_dir = operate_dir.join("controller");
290        fs::create_dir_all(&controller_dir).unwrap();
291        fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
292
293        let loader = AddonLoader::new(&addons_path);
294        loader.register().unwrap();
295
296        // 通过 autoload 解析类路径
297        let path = loader
298            .resolve_class("addons\\operate\\controller\\Order")
299            .unwrap();
300        assert!(path.is_some());
301        assert!(path.unwrap().exists());
302
303        // 通过 autoload 解析插件入口类
304        let plugin_path = loader.resolve_plugin("operate").unwrap();
305        assert!(plugin_path.is_some());
306        assert!(plugin_path.unwrap().exists());
307    }
308
309    /// 测试多个插件目录扫描
310    #[test]
311    fn test_multiple_addons_scan() {
312        let tmp = tempfile::tempdir().unwrap();
313        let addons_path = tmp.path().join("addons");
314
315        for name in &["operate", "cashier", "food", "erp"] {
316            let dir = addons_path.join(name);
317            fs::create_dir_all(&dir).unwrap();
318            fs::write(
319                dir.join("Plugin.php"),
320                format!(
321                    r#"
322public $info = [
323    'name' => '{}',
324    'status' => 1,
325];
326"#,
327                    name
328                ),
329            )
330            .unwrap();
331        }
332
333        let loader = AddonLoader::new(&addons_path);
334        let errors = loader.register().unwrap();
335        assert!(errors.is_empty());
336        assert_eq!(loader.count(), 4);
337        assert_eq!(loader.names(), vec!["cashier", "erp", "food", "operate"]);
338    }
339
340    /// 测试部分插件损坏时整体扫描不中断
341    #[test]
342    fn test_partial_failure_continues_scan() {
343        let tmp = tempfile::tempdir().unwrap();
344        let addons_path = tmp.path().join("addons");
345
346        // 好插件
347        let good_dir = addons_path.join("good");
348        fs::create_dir_all(&good_dir).unwrap();
349        fs::write(
350            good_dir.join("Plugin.php"),
351            r#"
352public $info = [
353    'name' => 'good',
354    'status' => 1,
355];
356"#,
357        )
358        .unwrap();
359
360        // 坏插件(无 $info 数组)
361        let bad_dir = addons_path.join("bad");
362        fs::create_dir_all(&bad_dir).unwrap();
363        fs::write(bad_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
364
365        let loader = AddonLoader::new(&addons_path);
366        let errors = loader.register().unwrap();
367
368        // bad 插件应记录错误
369        assert!(!errors.is_empty());
370        // good 插件应正常注册
371        assert_eq!(loader.count(), 1);
372        assert!(loader.exists("good"));
373        assert!(!loader.exists("bad"));
374    }
375
376    /// 测试空插件目录
377    #[test]
378    fn test_empty_addons_directory() {
379        let tmp = tempfile::tempdir().unwrap();
380        let addons_path = tmp.path().join("addons");
381        fs::create_dir_all(&addons_path).unwrap();
382
383        let loader = AddonLoader::new(&addons_path);
384        let errors = loader.register().unwrap();
385        assert!(errors.is_empty());
386        assert_eq!(loader.count(), 0);
387    }
388}