Skip to main content

sz_rust_addons_loader/
lib.rs

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