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 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 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 if name.starts_with('.') {
261 continue;
262 }
263
264 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 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 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 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 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 std::fs::write(tmp.path().join("BaseController.php"), "<?php // stub").unwrap();
597 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 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 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 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}