Skip to main content

sz_rust_addons_loader/
registry.rs

1//! 插件注册中心
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `think\addons` 中隐式的插件注册表:
6//!
7//! - `helper.php` 中 `get_addons_instance($name)` 的 `static $_addons = []` 单例缓存
8//! - `Service::loadEvent()` 中扫描插件目录构建钩子映射
9//! - `Service::loadService()` 中扫描插件目录 + `service.ini` 构建容器绑定
10//! - `Route::execute` 中通过 `get_addons_info($addon)` 检查插件状态
11//!
12//! ## 设计
13//!
14//! - 线程安全的插件注册表(`RwLock<HashMap<String, AddonManifest>>`)
15//! - 支持注册/查询/卸载插件
16//! - 支持状态管理(enabled/disabled,对齐 PHP `$info['status']`)
17//! - 支持按名称排序迭代(对齐 PHP `scandir` 顺序)
18
19use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21
22use parking_lot::RwLock;
23
24use crate::error::{AddonLoaderError, AddonLoaderResult};
25use crate::manifest::AddonManifest;
26
27/// 插件注册中心
28///
29/// 对齐 PHP `think\addons\Service` 中隐式的插件状态管理。
30///
31/// ## 线程安全
32///
33/// 使用 `RwLock<HashMap<String, AddonManifest>>` 实现,支持多读单写。
34///
35/// ## 用法
36///
37/// ```rust,ignore
38/// use sz_rust_addons_loader::registry::AddonRegistry;
39/// use sz_rust_addons_loader::manifest::AddonManifest;
40///
41/// let registry = AddonRegistry::new();
42/// let manifest = AddonManifest::new("operate");
43/// registry.register(manifest).unwrap();
44///
45/// assert!(registry.exists("operate"));
46/// assert_eq!(registry.count(), 1);
47/// ```
48#[derive(Debug)]
49pub struct AddonRegistry {
50    /// 插件清单映射(key = 插件名)
51    manifests: RwLock<HashMap<String, AddonManifest>>,
52}
53
54impl AddonRegistry {
55    /// 创建空注册中心
56    pub fn new() -> Self {
57        Self {
58            manifests: RwLock::new(HashMap::new()),
59        }
60    }
61
62    /// 注册插件(对齐 PHP `get_addons_instance` 缓存到 `$_addons[$name]`)
63    ///
64    /// ## 错误
65    ///
66    /// - `AddonNotFound`:插件名已存在(不覆盖)
67    #[tracing::instrument(skip(self, manifest))]
68    pub fn register(&self, manifest: AddonManifest) -> AddonLoaderResult<()> {
69        let mut manifests = self.manifests.write();
70        if manifests.contains_key(&manifest.name) {
71            return Err(AddonLoaderError::AddonNotFound(format!(
72                "addon '{}' already registered",
73                manifest.name
74            )));
75        }
76        manifests.insert(manifest.name.clone(), manifest);
77        Ok(())
78    }
79
80    /// 强制注册或更新插件(对齐 PHP `array_merge` 覆盖行为)
81    #[tracing::instrument(skip(self, manifest))]
82    pub fn upsert(&self, manifest: AddonManifest) {
83        let mut manifests = self.manifests.write();
84        manifests.insert(manifest.name.clone(), manifest);
85    }
86
87    /// 注销插件(对齐 PHP `unset($_addons[$name])`)
88    ///
89    /// ## 返回
90    ///
91    /// - `Ok(Some(manifest))`:插件已存在并已移除
92    /// - `Ok(None)`:插件不存在
93    #[tracing::instrument(skip(self))]
94    pub fn unregister(&self, name: &str) -> AddonLoaderResult<Option<AddonManifest>> {
95        let mut manifests = self.manifests.write();
96        Ok(manifests.remove(name))
97    }
98
99    /// 查询插件清单(对齐 PHP `get_addons_info($name)`)
100    ///
101    /// ## 错误
102    ///
103    /// - `AddonNotFound`:插件不存在
104    pub fn get(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
105        let manifests = self.manifests.read();
106        manifests
107            .get(name)
108            .cloned()
109            .ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))
110    }
111
112    /// 尝试查询插件清单(不返回错误)
113    pub fn try_get(&self, name: &str) -> Option<AddonManifest> {
114        let manifests = self.manifests.read();
115        manifests.get(name).cloned()
116    }
117
118    /// 判断插件是否存在(对齐 PHP `class_exists` 检查)
119    pub fn exists(&self, name: &str) -> bool {
120        let manifests = self.manifests.read();
121        manifests.contains_key(name)
122    }
123
124    /// 判断插件是否启用(对齐 PHP `Route::execute` 中 `!$info['status']` 检查)
125    ///
126    /// ## 返回
127    ///
128    /// - `Ok(true)`:插件存在且 status != 0
129    /// - `Ok(false)`:插件存在但 status == 0
130    /// - `Err(AddonNotFound)`:插件不存在
131    pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
132        let manifests = self.manifests.read();
133        let manifest = manifests
134            .get(name)
135            .ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
136        Ok(manifest.is_enabled())
137    }
138
139    /// 设置插件状态(对齐 PHP 修改 `$info['status']`)
140    ///
141    /// ## 参数
142    ///
143    /// - `name`:插件名
144    /// - `enabled`:true=1(启用),false=0(禁用)
145    #[tracing::instrument(skip(self))]
146    pub fn set_enabled(&self, name: &str, enabled: bool) -> AddonLoaderResult<()> {
147        let mut manifests = self.manifests.write();
148        let manifest = manifests
149            .get_mut(name)
150            .ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
151        manifest.status = if enabled { 1 } else { 0 };
152        Ok(())
153    }
154
155    /// 获取所有已注册的插件名(按字母序,对齐 PHP `scandir` 排序)
156    pub fn names(&self) -> Vec<String> {
157        let manifests = self.manifests.read();
158        let mut names: Vec<String> = manifests.keys().cloned().collect();
159        names.sort();
160        names
161    }
162
163    /// 获取所有已注册的插件清单(按名字序)
164    pub fn all(&self) -> Vec<AddonManifest> {
165        let manifests = self.manifests.read();
166        let mut list: Vec<AddonManifest> = manifests.values().cloned().collect();
167        list.sort_by(|a, b| a.name.cmp(&b.name));
168        list
169    }
170
171    /// 获取所有启用的插件清单(按名字序)
172    pub fn enabled_addons(&self) -> Vec<AddonManifest> {
173        let manifests = self.manifests.read();
174        let mut list: Vec<AddonManifest> = manifests
175            .values()
176            .filter(|m| m.is_enabled())
177            .cloned()
178            .collect();
179        list.sort_by(|a, b| a.name.cmp(&b.name));
180        list
181    }
182
183    /// 获取所有禁用的插件清单(按名字序)
184    pub fn disabled_addons(&self) -> Vec<AddonManifest> {
185        let manifests = self.manifests.read();
186        let mut list: Vec<AddonManifest> = manifests
187            .values()
188            .filter(|m| !m.is_enabled())
189            .cloned()
190            .collect();
191        list.sort_by(|a, b| a.name.cmp(&b.name));
192        list
193    }
194
195    /// 获取已注册插件数量
196    pub fn count(&self) -> usize {
197        let manifests = self.manifests.read();
198        manifests.len()
199    }
200
201    /// 清空注册中心
202    #[tracing::instrument(skip(self))]
203    pub fn clear(&self) {
204        let mut manifests = self.manifests.write();
205        manifests.clear();
206    }
207
208    /// 从插件目录批量加载并注册(对齐 PHP `Service::loadService` 扫描逻辑)
209    ///
210    /// ## 扫描规则
211    ///
212    /// 对齐 PHP `scandir($addons_path)`:
213    ///
214    /// 1. 扫描 `addons_path` 下的所有子目录
215    /// 2. 跳过 `.` `..` 及文件项
216    /// 3. 子目录必须包含 `Plugin.php`(对齐 PHP `is_file($addonDir . ucfirst($name) . '.php')`,Rust 侧统一使用 `Plugin.php`)
217    /// 4. 解析 `Plugin.php` 中的 `$info` 数组
218    /// 5. 注册到注册中心
219    ///
220    /// ## 错误处理
221    ///
222    /// - 单个插件解析失败不会中断整体扫描,但会记录到返回的错误列表
223    /// - 目录读取失败返回 `ScanDir` 错误
224    #[tracing::instrument(skip(self))]
225    pub async fn load_from_directory(
226        &self,
227        addons_path: &Path,
228    ) -> AddonLoaderResult<Vec<AddonLoaderError>> {
229        let mut errors = Vec::new();
230
231        let entries =
232            tokio::fs::read_dir(addons_path)
233                .await
234                .map_err(|e| AddonLoaderError::ScanDir {
235                    path: addons_path.display().to_string(),
236                    source: e,
237                })?;
238
239        let mut entries = entries;
240        while let Some(entry) =
241            entries
242                .next_entry()
243                .await
244                .map_err(|e| AddonLoaderError::ScanDir {
245                    path: addons_path.display().to_string(),
246                    source: e,
247                })?
248        {
249            let path = entry.path();
250            // 仅处理目录
251            if !path.is_dir() {
252                continue;
253            }
254
255            let name = match path.file_name().and_then(|n| n.to_str()) {
256                Some(n) => n.to_string(),
257                None => continue,
258            };
259
260            // 跳过隐藏目录(以 . 开头)
261            if name.starts_with('.') {
262                continue;
263            }
264
265            // 解析插件清单
266            match crate::manifest::parse_manifest(&path).await {
267                Ok(manifest) => {
268                    self.upsert(manifest);
269                }
270                Err(e) => {
271                    errors.push(e);
272                }
273            }
274        }
275
276        Ok(errors)
277    }
278
279    /// 获取插件文件路径(对齐 PHP `getAddonsPath() . $name . DIRECTORY_SEPARATOR`)
280    pub fn addon_path(&self, name: &str) -> AddonLoaderResult<PathBuf> {
281        let manifests = self.manifests.read();
282        let manifest = manifests
283            .get(name)
284            .ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
285        Ok(manifest.addon_path.clone())
286    }
287}
288
289impl Default for AddonRegistry {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    fn make_manifest(name: &str, status: i64) -> AddonManifest {
300        let mut m = AddonManifest::new(name);
301        m.status = status;
302        m
303    }
304
305    #[test]
306    fn test_new_empty() {
307        let registry = AddonRegistry::new();
308        assert_eq!(registry.count(), 0);
309        assert!(registry.names().is_empty());
310    }
311
312    #[test]
313    fn test_register_success() {
314        let registry = AddonRegistry::new();
315        let manifest = make_manifest("operate", 1);
316        assert!(registry.register(manifest).is_ok());
317        assert_eq!(registry.count(), 1);
318        assert!(registry.exists("operate"));
319    }
320
321    #[test]
322    fn test_register_duplicate_fails() {
323        let registry = AddonRegistry::new();
324        let manifest = make_manifest("operate", 1);
325        registry.register(manifest).unwrap();
326
327        let manifest2 = make_manifest("operate", 0);
328        let result = registry.register(manifest2);
329        assert!(result.is_err());
330        // 原状态保持不变
331        assert!(registry.is_enabled("operate").unwrap());
332    }
333
334    #[test]
335    fn test_upsert_overwrites() {
336        let registry = AddonRegistry::new();
337        registry.register(make_manifest("operate", 1)).unwrap();
338        registry.upsert(make_manifest("operate", 0));
339        assert!(!registry.is_enabled("operate").unwrap());
340    }
341
342    #[test]
343    fn test_unregister_existing() {
344        let registry = AddonRegistry::new();
345        registry.register(make_manifest("operate", 1)).unwrap();
346        let result = registry.unregister("operate").unwrap();
347        assert!(result.is_some());
348        assert_eq!(result.unwrap().name, "operate");
349        assert!(!registry.exists("operate"));
350    }
351
352    #[test]
353    fn test_unregister_nonexistent() {
354        let registry = AddonRegistry::new();
355        let result = registry.unregister("ghost").unwrap();
356        assert!(result.is_none());
357    }
358
359    #[test]
360    fn test_get_existing() {
361        let registry = AddonRegistry::new();
362        registry.register(make_manifest("operate", 1)).unwrap();
363        let manifest = registry.get("operate").unwrap();
364        assert_eq!(manifest.name, "operate");
365        assert_eq!(manifest.status, 1);
366    }
367
368    #[test]
369    fn test_get_nonexistent_returns_error() {
370        let registry = AddonRegistry::new();
371        let result = registry.get("ghost");
372        assert!(result.is_err());
373        match result.unwrap_err() {
374            AddonLoaderError::AddonNotFound(name) => assert_eq!(name, "ghost"),
375            other => panic!("expected AddonNotFound, got {:?}", other),
376        }
377    }
378
379    #[test]
380    fn test_try_get_existing() {
381        let registry = AddonRegistry::new();
382        registry.register(make_manifest("operate", 1)).unwrap();
383        let manifest = registry.try_get("operate");
384        assert!(manifest.is_some());
385        assert_eq!(manifest.unwrap().name, "operate");
386    }
387
388    #[test]
389    fn test_try_get_nonexistent() {
390        let registry = AddonRegistry::new();
391        let manifest = registry.try_get("ghost");
392        assert!(manifest.is_none());
393    }
394
395    #[test]
396    fn test_exists_true() {
397        let registry = AddonRegistry::new();
398        registry.register(make_manifest("operate", 1)).unwrap();
399        assert!(registry.exists("operate"));
400    }
401
402    #[test]
403    fn test_exists_false() {
404        let registry = AddonRegistry::new();
405        assert!(!registry.exists("operate"));
406    }
407
408    #[test]
409    fn test_is_enabled_enabled() {
410        let registry = AddonRegistry::new();
411        registry.register(make_manifest("operate", 1)).unwrap();
412        assert!(registry.is_enabled("operate").unwrap());
413    }
414
415    #[test]
416    fn test_is_enabled_disabled() {
417        let registry = AddonRegistry::new();
418        registry.register(make_manifest("operate", 0)).unwrap();
419        assert!(!registry.is_enabled("operate").unwrap());
420    }
421
422    #[test]
423    fn test_is_enabled_nonexistent() {
424        let registry = AddonRegistry::new();
425        let result = registry.is_enabled("ghost");
426        assert!(result.is_err());
427    }
428
429    #[test]
430    fn test_set_enabled_true() {
431        let registry = AddonRegistry::new();
432        registry.register(make_manifest("operate", 0)).unwrap();
433        assert!(!registry.is_enabled("operate").unwrap());
434
435        registry.set_enabled("operate", true).unwrap();
436        assert!(registry.is_enabled("operate").unwrap());
437    }
438
439    #[test]
440    fn test_set_enabled_false() {
441        let registry = AddonRegistry::new();
442        registry.register(make_manifest("operate", 1)).unwrap();
443        assert!(registry.is_enabled("operate").unwrap());
444
445        registry.set_enabled("operate", false).unwrap();
446        assert!(!registry.is_enabled("operate").unwrap());
447    }
448
449    #[test]
450    fn test_set_enabled_nonexistent() {
451        let registry = AddonRegistry::new();
452        let result = registry.set_enabled("ghost", true);
453        assert!(result.is_err());
454    }
455
456    #[test]
457    fn test_names_sorted() {
458        let registry = AddonRegistry::new();
459        registry.register(make_manifest("cashier", 1)).unwrap();
460        registry.register(make_manifest("operate", 1)).unwrap();
461        registry.register(make_manifest("food", 1)).unwrap();
462
463        let names = registry.names();
464        assert_eq!(names, vec!["cashier", "food", "operate"]);
465    }
466
467    #[test]
468    fn test_all_sorted() {
469        let registry = AddonRegistry::new();
470        registry.register(make_manifest("cashier", 1)).unwrap();
471        registry.register(make_manifest("operate", 1)).unwrap();
472
473        let all = registry.all();
474        assert_eq!(all.len(), 2);
475        assert_eq!(all[0].name, "cashier");
476        assert_eq!(all[1].name, "operate");
477    }
478
479    #[test]
480    fn test_enabled_addons_filtered() {
481        let registry = AddonRegistry::new();
482        registry.register(make_manifest("enabled1", 1)).unwrap();
483        registry.register(make_manifest("disabled1", 0)).unwrap();
484        registry.register(make_manifest("enabled2", 1)).unwrap();
485
486        let enabled = registry.enabled_addons();
487        assert_eq!(enabled.len(), 2);
488        assert_eq!(enabled[0].name, "enabled1");
489        assert_eq!(enabled[1].name, "enabled2");
490    }
491
492    #[test]
493    fn test_disabled_addons_filtered() {
494        let registry = AddonRegistry::new();
495        registry.register(make_manifest("enabled1", 1)).unwrap();
496        registry.register(make_manifest("disabled1", 0)).unwrap();
497        registry.register(make_manifest("disabled2", 0)).unwrap();
498
499        let disabled = registry.disabled_addons();
500        assert_eq!(disabled.len(), 2);
501        assert_eq!(disabled[0].name, "disabled1");
502        assert_eq!(disabled[1].name, "disabled2");
503    }
504
505    #[test]
506    fn test_count() {
507        let registry = AddonRegistry::new();
508        assert_eq!(registry.count(), 0);
509        registry.register(make_manifest("a", 1)).unwrap();
510        assert_eq!(registry.count(), 1);
511        registry.register(make_manifest("b", 1)).unwrap();
512        assert_eq!(registry.count(), 2);
513        registry.unregister("a").unwrap();
514        assert_eq!(registry.count(), 1);
515    }
516
517    #[test]
518    fn test_clear() {
519        let registry = AddonRegistry::new();
520        registry.register(make_manifest("a", 1)).unwrap();
521        registry.register(make_manifest("b", 1)).unwrap();
522        assert_eq!(registry.count(), 2);
523
524        registry.clear();
525        assert_eq!(registry.count(), 0);
526    }
527
528    #[test]
529    fn test_default_equals_new() {
530        let r1 = AddonRegistry::default();
531        let r2 = AddonRegistry::new();
532        assert_eq!(r1.count(), r2.count());
533    }
534
535    #[tokio::test]
536    async fn test_load_from_directory_empty() {
537        let tmp = tempfile::tempdir().expect("create tempdir");
538        let registry = AddonRegistry::new();
539        let errors = registry.load_from_directory(tmp.path()).await.unwrap();
540        assert!(errors.is_empty());
541        assert_eq!(registry.count(), 0);
542    }
543
544    #[tokio::test]
545    async fn test_load_from_directory_nonexistent() {
546        let registry = AddonRegistry::new();
547        let result = registry
548            .load_from_directory(Path::new("/nonexistent/path/12345"))
549            .await;
550        assert!(result.is_err());
551    }
552
553    #[tokio::test]
554    async fn test_load_from_directory_with_valid_plugin() {
555        let tmp = tempfile::tempdir().expect("create tempdir");
556        // 创建 operate 插件目录
557        let operate_dir = tmp.path().join("operate");
558        std::fs::create_dir_all(&operate_dir).unwrap();
559        let plugin_php = operate_dir.join("Plugin.php");
560        std::fs::write(
561            &plugin_php,
562            r#"
563public $info = [
564    'name' => 'operate',
565    'title' => '运营',
566    'status' => 1,
567];
568"#,
569        )
570        .unwrap();
571
572        // 创建 cashier 插件目录
573        let cashier_dir = tmp.path().join("cashier");
574        std::fs::create_dir_all(&cashier_dir).unwrap();
575        let plugin_php2 = cashier_dir.join("Plugin.php");
576        std::fs::write(
577            &plugin_php2,
578            r#"
579public $info = [
580    'name' => 'cashier',
581    'status' => 1,
582];
583"#,
584        )
585        .unwrap();
586
587        let registry = AddonRegistry::new();
588        let errors = registry.load_from_directory(tmp.path()).await.unwrap();
589        assert!(errors.is_empty(), "errors: {:?}", errors);
590        assert_eq!(registry.count(), 2);
591        assert!(registry.exists("operate"));
592        assert!(registry.exists("cashier"));
593    }
594
595    #[tokio::test]
596    async fn test_load_from_directory_skips_files() {
597        let tmp = tempfile::tempdir().expect("create tempdir");
598        // 创建文件(应被跳过)
599        std::fs::write(tmp.path().join("BaseController.php"), "<?php // stub").unwrap();
600        // 创建普通目录但无 Plugin.php(会记录错误)
601        std::fs::create_dir(tmp.path().join("empty_dir")).unwrap();
602
603        let registry = AddonRegistry::new();
604        let errors = registry.load_from_directory(tmp.path()).await.unwrap();
605        // empty_dir 无 Plugin.php → 错误
606        assert!(!errors.is_empty());
607        assert_eq!(registry.count(), 0);
608    }
609
610    #[tokio::test]
611    async fn test_load_from_directory_skips_hidden() {
612        let tmp = tempfile::tempdir().expect("create tempdir");
613        tokio::fs::create_dir(tmp.path().join(".hidden"))
614            .await
615            .unwrap();
616
617        let registry = AddonRegistry::new();
618        let errors = registry.load_from_directory(tmp.path()).await.unwrap();
619        // .hidden 目录应被跳过,不产生错误
620        assert!(errors.is_empty());
621        assert_eq!(registry.count(), 0);
622    }
623
624    #[test]
625    fn test_addon_path_existing() {
626        let registry = AddonRegistry::new();
627        let mut manifest = make_manifest("operate", 1);
628        manifest.addon_path = PathBuf::from("/addons/operate");
629        registry.register(manifest).unwrap();
630
631        let path = registry.addon_path("operate").unwrap();
632        assert_eq!(path, PathBuf::from("/addons/operate"));
633    }
634
635    #[test]
636    fn test_addon_path_nonexistent() {
637        let registry = AddonRegistry::new();
638        let result = registry.addon_path("ghost");
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_concurrent_read_access() {
644        let registry = std::sync::Arc::new(AddonRegistry::new());
645        registry.register(make_manifest("operate", 1)).unwrap();
646
647        let registry2 = registry.clone();
648        let handle = std::thread::spawn(move || {
649            // 并发读
650            registry2.try_get("operate")
651        });
652
653        let local_result = registry.try_get("operate");
654        let remote_result = handle.join().unwrap();
655
656        assert!(local_result.is_some());
657        assert!(remote_result.is_some());
658    }
659
660    #[test]
661    fn test_concurrent_write_access() {
662        let registry = std::sync::Arc::new(AddonRegistry::new());
663
664        let registry2 = registry.clone();
665        let handle = std::thread::spawn(move || registry2.register(make_manifest("operate", 1)));
666
667        let local_result = registry.register(make_manifest("cashier", 1));
668        let remote_result = handle.join().unwrap();
669
670        assert!(local_result.is_ok());
671        assert!(remote_result.is_ok());
672        assert_eq!(registry.count(), 2);
673    }
674}