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