use std::collections::HashMap;
use std::path::{Path, PathBuf};
use parking_lot::RwLock;
use crate::error::{AddonLoaderError, AddonLoaderResult};
use crate::manifest::AddonManifest;
#[derive(Debug)]
pub struct AddonRegistry {
manifests: RwLock<HashMap<String, AddonManifest>>,
}
impl AddonRegistry {
pub fn new() -> Self {
Self {
manifests: RwLock::new(HashMap::new()),
}
}
#[tracing::instrument(skip(self, manifest))]
pub fn register(&self, manifest: AddonManifest) -> AddonLoaderResult<()> {
let mut manifests = self.manifests.write();
if manifests.contains_key(&manifest.name) {
return Err(AddonLoaderError::AddonNotFound(format!(
"addon '{}' already registered",
manifest.name
)));
}
manifests.insert(manifest.name.clone(), manifest);
Ok(())
}
#[tracing::instrument(skip(self, manifest))]
pub fn upsert(&self, manifest: AddonManifest) {
let mut manifests = self.manifests.write();
manifests.insert(manifest.name.clone(), manifest);
}
#[tracing::instrument(skip(self))]
pub fn unregister(&self, name: &str) -> AddonLoaderResult<Option<AddonManifest>> {
let mut manifests = self.manifests.write();
Ok(manifests.remove(name))
}
pub fn get(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
let manifests = self.manifests.read();
manifests
.get(name)
.cloned()
.ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))
}
pub fn try_get(&self, name: &str) -> Option<AddonManifest> {
let manifests = self.manifests.read();
manifests.get(name).cloned()
}
pub fn exists(&self, name: &str) -> bool {
let manifests = self.manifests.read();
manifests.contains_key(name)
}
pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
let manifests = self.manifests.read();
let manifest = manifests
.get(name)
.ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
Ok(manifest.is_enabled())
}
#[tracing::instrument(skip(self))]
pub fn set_enabled(&self, name: &str, enabled: bool) -> AddonLoaderResult<()> {
let mut manifests = self.manifests.write();
let manifest = manifests
.get_mut(name)
.ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
manifest.status = if enabled { 1 } else { 0 };
Ok(())
}
pub fn names(&self) -> Vec<String> {
let manifests = self.manifests.read();
let mut names: Vec<String> = manifests.keys().cloned().collect();
names.sort();
names
}
pub fn all(&self) -> Vec<AddonManifest> {
let manifests = self.manifests.read();
let mut list: Vec<AddonManifest> = manifests.values().cloned().collect();
list.sort_by(|a, b| a.name.cmp(&b.name));
list
}
pub fn enabled_addons(&self) -> Vec<AddonManifest> {
let manifests = self.manifests.read();
let mut list: Vec<AddonManifest> = manifests
.values()
.filter(|m| m.is_enabled())
.cloned()
.collect();
list.sort_by(|a, b| a.name.cmp(&b.name));
list
}
pub fn disabled_addons(&self) -> Vec<AddonManifest> {
let manifests = self.manifests.read();
let mut list: Vec<AddonManifest> = manifests
.values()
.filter(|m| !m.is_enabled())
.cloned()
.collect();
list.sort_by(|a, b| a.name.cmp(&b.name));
list
}
pub fn count(&self) -> usize {
let manifests = self.manifests.read();
manifests.len()
}
#[tracing::instrument(skip(self))]
pub fn clear(&self) {
let mut manifests = self.manifests.write();
manifests.clear();
}
#[tracing::instrument(skip(self))]
pub fn load_from_directory(
&self,
addons_path: &Path,
) -> AddonLoaderResult<Vec<AddonLoaderError>> {
let mut errors = Vec::new();
let entries = std::fs::read_dir(addons_path).map_err(|e| AddonLoaderError::ScanDir {
path: addons_path.display().to_string(),
source: e,
})?;
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => {
errors.push(AddonLoaderError::ScanDir {
path: addons_path.display().to_string(),
source: e,
});
continue;
}
};
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if name.starts_with('.') {
continue;
}
match crate::manifest::parse_manifest(&path) {
Ok(manifest) => {
self.upsert(manifest);
}
Err(e) => {
errors.push(e);
}
}
}
Ok(errors)
}
pub fn addon_path(&self, name: &str) -> AddonLoaderResult<PathBuf> {
let manifests = self.manifests.read();
let manifest = manifests
.get(name)
.ok_or_else(|| AddonLoaderError::AddonNotFound(name.to_string()))?;
Ok(manifest.addon_path.clone())
}
}
impl Default for AddonRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_manifest(name: &str, status: i64) -> AddonManifest {
let mut m = AddonManifest::new(name);
m.status = status;
m
}
#[test]
fn test_new_empty() {
let registry = AddonRegistry::new();
assert_eq!(registry.count(), 0);
assert!(registry.names().is_empty());
}
#[test]
fn test_register_success() {
let registry = AddonRegistry::new();
let manifest = make_manifest("operate", 1);
assert!(registry.register(manifest).is_ok());
assert_eq!(registry.count(), 1);
assert!(registry.exists("operate"));
}
#[test]
fn test_register_duplicate_fails() {
let registry = AddonRegistry::new();
let manifest = make_manifest("operate", 1);
registry.register(manifest).unwrap();
let manifest2 = make_manifest("operate", 0);
let result = registry.register(manifest2);
assert!(result.is_err());
assert!(registry.is_enabled("operate").unwrap());
}
#[test]
fn test_upsert_overwrites() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
registry.upsert(make_manifest("operate", 0));
assert!(!registry.is_enabled("operate").unwrap());
}
#[test]
fn test_unregister_existing() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
let result = registry.unregister("operate").unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().name, "operate");
assert!(!registry.exists("operate"));
}
#[test]
fn test_unregister_nonexistent() {
let registry = AddonRegistry::new();
let result = registry.unregister("ghost").unwrap();
assert!(result.is_none());
}
#[test]
fn test_get_existing() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
let manifest = registry.get("operate").unwrap();
assert_eq!(manifest.name, "operate");
assert_eq!(manifest.status, 1);
}
#[test]
fn test_get_nonexistent_returns_error() {
let registry = AddonRegistry::new();
let result = registry.get("ghost");
assert!(result.is_err());
match result.unwrap_err() {
AddonLoaderError::AddonNotFound(name) => assert_eq!(name, "ghost"),
other => panic!("expected AddonNotFound, got {:?}", other),
}
}
#[test]
fn test_try_get_existing() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
let manifest = registry.try_get("operate");
assert!(manifest.is_some());
assert_eq!(manifest.unwrap().name, "operate");
}
#[test]
fn test_try_get_nonexistent() {
let registry = AddonRegistry::new();
let manifest = registry.try_get("ghost");
assert!(manifest.is_none());
}
#[test]
fn test_exists_true() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
assert!(registry.exists("operate"));
}
#[test]
fn test_exists_false() {
let registry = AddonRegistry::new();
assert!(!registry.exists("operate"));
}
#[test]
fn test_is_enabled_enabled() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
assert!(registry.is_enabled("operate").unwrap());
}
#[test]
fn test_is_enabled_disabled() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 0)).unwrap();
assert!(!registry.is_enabled("operate").unwrap());
}
#[test]
fn test_is_enabled_nonexistent() {
let registry = AddonRegistry::new();
let result = registry.is_enabled("ghost");
assert!(result.is_err());
}
#[test]
fn test_set_enabled_true() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 0)).unwrap();
assert!(!registry.is_enabled("operate").unwrap());
registry.set_enabled("operate", true).unwrap();
assert!(registry.is_enabled("operate").unwrap());
}
#[test]
fn test_set_enabled_false() {
let registry = AddonRegistry::new();
registry.register(make_manifest("operate", 1)).unwrap();
assert!(registry.is_enabled("operate").unwrap());
registry.set_enabled("operate", false).unwrap();
assert!(!registry.is_enabled("operate").unwrap());
}
#[test]
fn test_set_enabled_nonexistent() {
let registry = AddonRegistry::new();
let result = registry.set_enabled("ghost", true);
assert!(result.is_err());
}
#[test]
fn test_names_sorted() {
let registry = AddonRegistry::new();
registry.register(make_manifest("cashier", 1)).unwrap();
registry.register(make_manifest("operate", 1)).unwrap();
registry.register(make_manifest("food", 1)).unwrap();
let names = registry.names();
assert_eq!(names, vec!["cashier", "food", "operate"]);
}
#[test]
fn test_all_sorted() {
let registry = AddonRegistry::new();
registry.register(make_manifest("cashier", 1)).unwrap();
registry.register(make_manifest("operate", 1)).unwrap();
let all = registry.all();
assert_eq!(all.len(), 2);
assert_eq!(all[0].name, "cashier");
assert_eq!(all[1].name, "operate");
}
#[test]
fn test_enabled_addons_filtered() {
let registry = AddonRegistry::new();
registry.register(make_manifest("enabled1", 1)).unwrap();
registry.register(make_manifest("disabled1", 0)).unwrap();
registry.register(make_manifest("enabled2", 1)).unwrap();
let enabled = registry.enabled_addons();
assert_eq!(enabled.len(), 2);
assert_eq!(enabled[0].name, "enabled1");
assert_eq!(enabled[1].name, "enabled2");
}
#[test]
fn test_disabled_addons_filtered() {
let registry = AddonRegistry::new();
registry.register(make_manifest("enabled1", 1)).unwrap();
registry.register(make_manifest("disabled1", 0)).unwrap();
registry.register(make_manifest("disabled2", 0)).unwrap();
let disabled = registry.disabled_addons();
assert_eq!(disabled.len(), 2);
assert_eq!(disabled[0].name, "disabled1");
assert_eq!(disabled[1].name, "disabled2");
}
#[test]
fn test_count() {
let registry = AddonRegistry::new();
assert_eq!(registry.count(), 0);
registry.register(make_manifest("a", 1)).unwrap();
assert_eq!(registry.count(), 1);
registry.register(make_manifest("b", 1)).unwrap();
assert_eq!(registry.count(), 2);
registry.unregister("a").unwrap();
assert_eq!(registry.count(), 1);
}
#[test]
fn test_clear() {
let registry = AddonRegistry::new();
registry.register(make_manifest("a", 1)).unwrap();
registry.register(make_manifest("b", 1)).unwrap();
assert_eq!(registry.count(), 2);
registry.clear();
assert_eq!(registry.count(), 0);
}
#[test]
fn test_default_equals_new() {
let r1 = AddonRegistry::default();
let r2 = AddonRegistry::new();
assert_eq!(r1.count(), r2.count());
}
#[test]
fn test_load_from_directory_empty() {
let tmp = tempfile::tempdir().expect("create tempdir");
let registry = AddonRegistry::new();
let errors = registry.load_from_directory(tmp.path()).unwrap();
assert!(errors.is_empty());
assert_eq!(registry.count(), 0);
}
#[test]
fn test_load_from_directory_nonexistent() {
let registry = AddonRegistry::new();
let result = registry.load_from_directory(Path::new("/nonexistent/path/12345"));
assert!(result.is_err());
}
#[test]
fn test_load_from_directory_with_valid_plugin() {
let tmp = tempfile::tempdir().expect("create tempdir");
let operate_dir = tmp.path().join("operate");
std::fs::create_dir_all(&operate_dir).unwrap();
let plugin_php = operate_dir.join("Plugin.php");
std::fs::write(
&plugin_php,
r#"
public $info = [
'name' => 'operate',
'title' => '运营',
'status' => 1,
];
"#,
)
.unwrap();
let cashier_dir = tmp.path().join("cashier");
std::fs::create_dir_all(&cashier_dir).unwrap();
let plugin_php2 = cashier_dir.join("Plugin.php");
std::fs::write(
&plugin_php2,
r#"
public $info = [
'name' => 'cashier',
'status' => 1,
];
"#,
)
.unwrap();
let registry = AddonRegistry::new();
let errors = registry.load_from_directory(tmp.path()).unwrap();
assert!(errors.is_empty(), "errors: {:?}", errors);
assert_eq!(registry.count(), 2);
assert!(registry.exists("operate"));
assert!(registry.exists("cashier"));
}
#[test]
fn test_load_from_directory_skips_files() {
let tmp = tempfile::tempdir().expect("create tempdir");
std::fs::write(tmp.path().join("BaseController.php"), "<?php // stub").unwrap();
std::fs::create_dir(tmp.path().join("empty_dir")).unwrap();
let registry = AddonRegistry::new();
let errors = registry.load_from_directory(tmp.path()).unwrap();
assert!(!errors.is_empty());
assert_eq!(registry.count(), 0);
}
#[test]
fn test_load_from_directory_skips_hidden() {
let tmp = tempfile::tempdir().expect("create tempdir");
std::fs::create_dir(tmp.path().join(".hidden")).unwrap();
let registry = AddonRegistry::new();
let errors = registry.load_from_directory(tmp.path()).unwrap();
assert!(errors.is_empty());
assert_eq!(registry.count(), 0);
}
#[test]
fn test_addon_path_existing() {
let registry = AddonRegistry::new();
let mut manifest = make_manifest("operate", 1);
manifest.addon_path = PathBuf::from("/addons/operate");
registry.register(manifest).unwrap();
let path = registry.addon_path("operate").unwrap();
assert_eq!(path, PathBuf::from("/addons/operate"));
}
#[test]
fn test_addon_path_nonexistent() {
let registry = AddonRegistry::new();
let result = registry.addon_path("ghost");
assert!(result.is_err());
}
#[test]
fn test_concurrent_read_access() {
let registry = std::sync::Arc::new(AddonRegistry::new());
registry.register(make_manifest("operate", 1)).unwrap();
let registry2 = registry.clone();
let handle = std::thread::spawn(move || {
registry2.try_get("operate")
});
let local_result = registry.try_get("operate");
let remote_result = handle.join().unwrap();
assert!(local_result.is_some());
assert!(remote_result.is_some());
}
#[test]
fn test_concurrent_write_access() {
let registry = std::sync::Arc::new(AddonRegistry::new());
let registry2 = registry.clone();
let handle = std::thread::spawn(move || registry2.register(make_manifest("operate", 1)));
let local_result = registry.register(make_manifest("cashier", 1));
let remote_result = handle.join().unwrap();
assert!(local_result.is_ok());
assert!(remote_result.is_ok());
assert_eq!(registry.count(), 2);
}
}