1use 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#[derive(Debug)]
49pub struct AddonRegistry {
50 manifests: RwLock<HashMap<String, AddonManifest>>,
52}
53
54impl AddonRegistry {
55 pub fn new() -> Self {
57 Self {
58 manifests: RwLock::new(HashMap::new()),
59 }
60 }
61
62 #[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 #[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 #[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 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 pub fn try_get(&self, name: &str) -> Option<AddonManifest> {
114 let manifests = self.manifests.read();
115 manifests.get(name).cloned()
116 }
117
118 pub fn exists(&self, name: &str) -> bool {
120 let manifests = self.manifests.read();
121 manifests.contains_key(name)
122 }
123
124 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 #[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 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 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 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 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 pub fn count(&self) -> usize {
197 let manifests = self.manifests.read();
198 manifests.len()
199 }
200
201 #[tracing::instrument(skip(self))]
203 pub fn clear(&self) {
204 let mut manifests = self.manifests.write();
205 manifests.clear();
206 }
207
208 #[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 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 if name.starts_with('.') {
262 continue;
263 }
264
265 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 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 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 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 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 std::fs::write(tmp.path().join("BaseController.php"), "<?php // stub").unwrap();
600 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 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 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 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}