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 fn load_from_directory(
226        &self,
227        addons_path: &Path,
228    ) -> AddonLoaderResult<Vec<AddonLoaderError>> {
229        let mut errors = Vec::new();
230
231        let entries = std::fs::read_dir(addons_path).map_err(|e| AddonLoaderError::ScanDir {
232            path: addons_path.display().to_string(),
233            source: e,
234        })?;
235
236        for entry in entries {
237            let entry = match entry {
238                Ok(e) => e,
239                Err(e) => {
240                    errors.push(AddonLoaderError::ScanDir {
241                        path: addons_path.display().to_string(),
242                        source: e,
243                    });
244                    continue;
245                }
246            };
247
248            let path = entry.path();
249            // 仅处理目录
250            if !path.is_dir() {
251                continue;
252            }
253
254            let name = match path.file_name().and_then(|n| n.to_str()) {
255                Some(n) => n.to_string(),
256                None => continue,
257            };
258
259            // 跳过隐藏目录(以 . 开头)
260            if name.starts_with('.') {
261                continue;
262            }
263
264            // 解析插件清单
265            match crate::manifest::parse_manifest(&path) {
266                Ok(manifest) => {
267                    self.upsert(manifest);
268                }
269                Err(e) => {
270                    errors.push(e);
271                }
272            }
273        }
274
275        Ok(errors)
276    }
277
278    /// 获取插件文件路径(对齐 PHP `getAddonsPath() . $name . DIRECTORY_SEPARATOR`)
279    pub fn addon_path(&self, name: &str) -> AddonLoaderResult<PathBuf> {
280        let manifests = self.manifests.read();
281        let manifest = manifests
282            .get(name)
283            .ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
284        Ok(manifest.addon_path.clone())
285    }
286}
287
288impl Default for AddonRegistry {
289    fn default() -> Self {
290        Self::new()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    fn make_manifest(name: &str, status: i64) -> AddonManifest {
299        let mut m = AddonManifest::new(name);
300        m.status = status;
301        m
302    }
303
304    #[test]
305    fn test_new_empty() {
306        let registry = AddonRegistry::new();
307        assert_eq!(registry.count(), 0);
308        assert!(registry.names().is_empty());
309    }
310
311    #[test]
312    fn test_register_success() {
313        let registry = AddonRegistry::new();
314        let manifest = make_manifest("operate", 1);
315        assert!(registry.register(manifest).is_ok());
316        assert_eq!(registry.count(), 1);
317        assert!(registry.exists("operate"));
318    }
319
320    #[test]
321    fn test_register_duplicate_fails() {
322        let registry = AddonRegistry::new();
323        let manifest = make_manifest("operate", 1);
324        registry.register(manifest).unwrap();
325
326        let manifest2 = make_manifest("operate", 0);
327        let result = registry.register(manifest2);
328        assert!(result.is_err());
329        // 原状态保持不变
330        assert!(registry.is_enabled("operate").unwrap());
331    }
332
333    #[test]
334    fn test_upsert_overwrites() {
335        let registry = AddonRegistry::new();
336        registry.register(make_manifest("operate", 1)).unwrap();
337        registry.upsert(make_manifest("operate", 0));
338        assert!(!registry.is_enabled("operate").unwrap());
339    }
340
341    #[test]
342    fn test_unregister_existing() {
343        let registry = AddonRegistry::new();
344        registry.register(make_manifest("operate", 1)).unwrap();
345        let result = registry.unregister("operate").unwrap();
346        assert!(result.is_some());
347        assert_eq!(result.unwrap().name, "operate");
348        assert!(!registry.exists("operate"));
349    }
350
351    #[test]
352    fn test_unregister_nonexistent() {
353        let registry = AddonRegistry::new();
354        let result = registry.unregister("ghost").unwrap();
355        assert!(result.is_none());
356    }
357
358    #[test]
359    fn test_get_existing() {
360        let registry = AddonRegistry::new();
361        registry.register(make_manifest("operate", 1)).unwrap();
362        let manifest = registry.get("operate").unwrap();
363        assert_eq!(manifest.name, "operate");
364        assert_eq!(manifest.status, 1);
365    }
366
367    #[test]
368    fn test_get_nonexistent_returns_error() {
369        let registry = AddonRegistry::new();
370        let result = registry.get("ghost");
371        assert!(result.is_err());
372        match result.unwrap_err() {
373            AddonLoaderError::AddonNotFound(name) => assert_eq!(name, "ghost"),
374            other => panic!("expected AddonNotFound, got {:?}", other),
375        }
376    }
377
378    #[test]
379    fn test_try_get_existing() {
380        let registry = AddonRegistry::new();
381        registry.register(make_manifest("operate", 1)).unwrap();
382        let manifest = registry.try_get("operate");
383        assert!(manifest.is_some());
384        assert_eq!(manifest.unwrap().name, "operate");
385    }
386
387    #[test]
388    fn test_try_get_nonexistent() {
389        let registry = AddonRegistry::new();
390        let manifest = registry.try_get("ghost");
391        assert!(manifest.is_none());
392    }
393
394    #[test]
395    fn test_exists_true() {
396        let registry = AddonRegistry::new();
397        registry.register(make_manifest("operate", 1)).unwrap();
398        assert!(registry.exists("operate"));
399    }
400
401    #[test]
402    fn test_exists_false() {
403        let registry = AddonRegistry::new();
404        assert!(!registry.exists("operate"));
405    }
406
407    #[test]
408    fn test_is_enabled_enabled() {
409        let registry = AddonRegistry::new();
410        registry.register(make_manifest("operate", 1)).unwrap();
411        assert!(registry.is_enabled("operate").unwrap());
412    }
413
414    #[test]
415    fn test_is_enabled_disabled() {
416        let registry = AddonRegistry::new();
417        registry.register(make_manifest("operate", 0)).unwrap();
418        assert!(!registry.is_enabled("operate").unwrap());
419    }
420
421    #[test]
422    fn test_is_enabled_nonexistent() {
423        let registry = AddonRegistry::new();
424        let result = registry.is_enabled("ghost");
425        assert!(result.is_err());
426    }
427
428    #[test]
429    fn test_set_enabled_true() {
430        let registry = AddonRegistry::new();
431        registry.register(make_manifest("operate", 0)).unwrap();
432        assert!(!registry.is_enabled("operate").unwrap());
433
434        registry.set_enabled("operate", true).unwrap();
435        assert!(registry.is_enabled("operate").unwrap());
436    }
437
438    #[test]
439    fn test_set_enabled_false() {
440        let registry = AddonRegistry::new();
441        registry.register(make_manifest("operate", 1)).unwrap();
442        assert!(registry.is_enabled("operate").unwrap());
443
444        registry.set_enabled("operate", false).unwrap();
445        assert!(!registry.is_enabled("operate").unwrap());
446    }
447
448    #[test]
449    fn test_set_enabled_nonexistent() {
450        let registry = AddonRegistry::new();
451        let result = registry.set_enabled("ghost", true);
452        assert!(result.is_err());
453    }
454
455    #[test]
456    fn test_names_sorted() {
457        let registry = AddonRegistry::new();
458        registry.register(make_manifest("cashier", 1)).unwrap();
459        registry.register(make_manifest("operate", 1)).unwrap();
460        registry.register(make_manifest("food", 1)).unwrap();
461
462        let names = registry.names();
463        assert_eq!(names, vec!["cashier", "food", "operate"]);
464    }
465
466    #[test]
467    fn test_all_sorted() {
468        let registry = AddonRegistry::new();
469        registry.register(make_manifest("cashier", 1)).unwrap();
470        registry.register(make_manifest("operate", 1)).unwrap();
471
472        let all = registry.all();
473        assert_eq!(all.len(), 2);
474        assert_eq!(all[0].name, "cashier");
475        assert_eq!(all[1].name, "operate");
476    }
477
478    #[test]
479    fn test_enabled_addons_filtered() {
480        let registry = AddonRegistry::new();
481        registry.register(make_manifest("enabled1", 1)).unwrap();
482        registry.register(make_manifest("disabled1", 0)).unwrap();
483        registry.register(make_manifest("enabled2", 1)).unwrap();
484
485        let enabled = registry.enabled_addons();
486        assert_eq!(enabled.len(), 2);
487        assert_eq!(enabled[0].name, "enabled1");
488        assert_eq!(enabled[1].name, "enabled2");
489    }
490
491    #[test]
492    fn test_disabled_addons_filtered() {
493        let registry = AddonRegistry::new();
494        registry.register(make_manifest("enabled1", 1)).unwrap();
495        registry.register(make_manifest("disabled1", 0)).unwrap();
496        registry.register(make_manifest("disabled2", 0)).unwrap();
497
498        let disabled = registry.disabled_addons();
499        assert_eq!(disabled.len(), 2);
500        assert_eq!(disabled[0].name, "disabled1");
501        assert_eq!(disabled[1].name, "disabled2");
502    }
503
504    #[test]
505    fn test_count() {
506        let registry = AddonRegistry::new();
507        assert_eq!(registry.count(), 0);
508        registry.register(make_manifest("a", 1)).unwrap();
509        assert_eq!(registry.count(), 1);
510        registry.register(make_manifest("b", 1)).unwrap();
511        assert_eq!(registry.count(), 2);
512        registry.unregister("a").unwrap();
513        assert_eq!(registry.count(), 1);
514    }
515
516    #[test]
517    fn test_clear() {
518        let registry = AddonRegistry::new();
519        registry.register(make_manifest("a", 1)).unwrap();
520        registry.register(make_manifest("b", 1)).unwrap();
521        assert_eq!(registry.count(), 2);
522
523        registry.clear();
524        assert_eq!(registry.count(), 0);
525    }
526
527    #[test]
528    fn test_default_equals_new() {
529        let r1 = AddonRegistry::default();
530        let r2 = AddonRegistry::new();
531        assert_eq!(r1.count(), r2.count());
532    }
533
534    #[test]
535    fn test_load_from_directory_empty() {
536        let tmp = tempfile::tempdir().expect("create tempdir");
537        let registry = AddonRegistry::new();
538        let errors = registry.load_from_directory(tmp.path()).unwrap();
539        assert!(errors.is_empty());
540        assert_eq!(registry.count(), 0);
541    }
542
543    #[test]
544    fn test_load_from_directory_nonexistent() {
545        let registry = AddonRegistry::new();
546        let result = registry.load_from_directory(Path::new("/nonexistent/path/12345"));
547        assert!(result.is_err());
548    }
549
550    #[test]
551    fn test_load_from_directory_with_valid_plugin() {
552        let tmp = tempfile::tempdir().expect("create tempdir");
553        // 创建 operate 插件目录
554        let operate_dir = tmp.path().join("operate");
555        std::fs::create_dir_all(&operate_dir).unwrap();
556        let plugin_php = operate_dir.join("Plugin.php");
557        std::fs::write(
558            &plugin_php,
559            r#"
560public $info = [
561    'name' => 'operate',
562    'title' => '运营',
563    'status' => 1,
564];
565"#,
566        )
567        .unwrap();
568
569        // 创建 cashier 插件目录
570        let cashier_dir = tmp.path().join("cashier");
571        std::fs::create_dir_all(&cashier_dir).unwrap();
572        let plugin_php2 = cashier_dir.join("Plugin.php");
573        std::fs::write(
574            &plugin_php2,
575            r#"
576public $info = [
577    'name' => 'cashier',
578    'status' => 1,
579];
580"#,
581        )
582        .unwrap();
583
584        let registry = AddonRegistry::new();
585        let errors = registry.load_from_directory(tmp.path()).unwrap();
586        assert!(errors.is_empty(), "errors: {:?}", errors);
587        assert_eq!(registry.count(), 2);
588        assert!(registry.exists("operate"));
589        assert!(registry.exists("cashier"));
590    }
591
592    #[test]
593    fn test_load_from_directory_skips_files() {
594        let tmp = tempfile::tempdir().expect("create tempdir");
595        // 创建文件(应被跳过)
596        std::fs::write(tmp.path().join("BaseController.php"), "<?php // stub").unwrap();
597        // 创建普通目录但无 Plugin.php(会记录错误)
598        std::fs::create_dir(tmp.path().join("empty_dir")).unwrap();
599
600        let registry = AddonRegistry::new();
601        let errors = registry.load_from_directory(tmp.path()).unwrap();
602        // empty_dir 无 Plugin.php → 错误
603        assert!(!errors.is_empty());
604        assert_eq!(registry.count(), 0);
605    }
606
607    #[test]
608    fn test_load_from_directory_skips_hidden() {
609        let tmp = tempfile::tempdir().expect("create tempdir");
610        std::fs::create_dir(tmp.path().join(".hidden")).unwrap();
611
612        let registry = AddonRegistry::new();
613        let errors = registry.load_from_directory(tmp.path()).unwrap();
614        // .hidden 目录应被跳过,不产生错误
615        assert!(errors.is_empty());
616        assert_eq!(registry.count(), 0);
617    }
618
619    #[test]
620    fn test_addon_path_existing() {
621        let registry = AddonRegistry::new();
622        let mut manifest = make_manifest("operate", 1);
623        manifest.addon_path = PathBuf::from("/addons/operate");
624        registry.register(manifest).unwrap();
625
626        let path = registry.addon_path("operate").unwrap();
627        assert_eq!(path, PathBuf::from("/addons/operate"));
628    }
629
630    #[test]
631    fn test_addon_path_nonexistent() {
632        let registry = AddonRegistry::new();
633        let result = registry.addon_path("ghost");
634        assert!(result.is_err());
635    }
636
637    #[test]
638    fn test_concurrent_read_access() {
639        let registry = std::sync::Arc::new(AddonRegistry::new());
640        registry.register(make_manifest("operate", 1)).unwrap();
641
642        let registry2 = registry.clone();
643        let handle = std::thread::spawn(move || {
644            // 并发读
645            registry2.try_get("operate")
646        });
647
648        let local_result = registry.try_get("operate");
649        let remote_result = handle.join().unwrap();
650
651        assert!(local_result.is_some());
652        assert!(remote_result.is_some());
653    }
654
655    #[test]
656    fn test_concurrent_write_access() {
657        let registry = std::sync::Arc::new(AddonRegistry::new());
658
659        let registry2 = registry.clone();
660        let handle = std::thread::spawn(move || registry2.register(make_manifest("operate", 1)));
661
662        let local_result = registry.register(make_manifest("cashier", 1));
663        let remote_result = handle.join().unwrap();
664
665        assert!(local_result.is_ok());
666        assert!(remote_result.is_ok());
667        assert_eq!(registry.count(), 2);
668    }
669}