use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::autoload::AddonAutoload;
use crate::error::{AddonLoaderError, AddonLoaderResult};
use crate::manifest::AddonManifest;
use crate::registry::AddonRegistry;
#[derive(Debug)]
pub struct AddonLoader {
addons_path: PathBuf,
registry: Arc<AddonRegistry>,
autoload: AddonAutoload,
}
impl AddonLoader {
pub fn new(addons_path: impl Into<PathBuf>) -> Self {
let addons_path = addons_path.into();
let autoload = AddonAutoload::new(&addons_path);
Self {
addons_path,
registry: Arc::new(AddonRegistry::new()),
autoload,
}
}
pub fn addons_path(&self) -> &Path {
&self.addons_path
}
pub fn registry(&self) -> &AddonRegistry {
&self.registry
}
pub fn autoload(&self) -> &AddonAutoload {
&self.autoload
}
#[tracing::instrument(skip(self))]
pub fn register(&self) -> AddonLoaderResult<Vec<AddonLoaderError>> {
self.registry.load_from_directory(&self.addons_path)
}
#[tracing::instrument(skip(self))]
pub fn parse_route(&self, url: &str) -> AddonLoaderResult<crate::route::AddonRoute> {
crate::route::parse_route(url, &self.registry, &self.autoload)
}
pub fn names(&self) -> Vec<String> {
self.registry.names()
}
pub fn all_addons(&self) -> Vec<AddonManifest> {
self.registry.all()
}
pub fn enabled_addons(&self) -> Vec<AddonManifest> {
self.registry.enabled_addons()
}
pub fn disabled_addons(&self) -> Vec<AddonManifest> {
self.registry.disabled_addons()
}
pub fn exists(&self, name: &str) -> bool {
self.registry.exists(name)
}
pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
self.registry.is_enabled(name)
}
pub fn get_manifest(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
self.registry.get(name)
}
pub fn count(&self) -> usize {
self.registry.count()
}
pub fn resolve_class(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
self.autoload.resolve(class)
}
pub fn resolve_controller(
&self,
addon: &str,
controller: &str,
) -> AddonLoaderResult<Option<PathBuf>> {
self.autoload.resolve_controller(addon, controller)
}
pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
self.autoload.resolve_plugin(addon)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_test_env() -> (tempfile::TempDir, AddonLoader) {
let tmp = tempfile::tempdir().expect("create tempdir");
let addons_path = tmp.path().join("addons");
let operate_dir = addons_path.join("operate");
fs::create_dir_all(&operate_dir).unwrap();
fs::write(
operate_dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'operate',
'title' => '运营管理',
'status' => 1,
];
"#,
)
.unwrap();
let controller_dir = operate_dir.join("controller");
fs::create_dir_all(&controller_dir).unwrap();
fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
let admin_dir = controller_dir.join("admin");
fs::create_dir_all(&admin_dir).unwrap();
fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
let cashier_dir = addons_path.join("cashier");
fs::create_dir_all(&cashier_dir).unwrap();
fs::write(
cashier_dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'cashier',
'status' => 0,
];
"#,
)
.unwrap();
let loader = AddonLoader::new(&addons_path);
(tmp, loader)
}
#[test]
fn test_new_loader() {
let loader = AddonLoader::new("/addons");
assert_eq!(loader.addons_path(), Path::new("/addons"));
assert_eq!(loader.count(), 0);
}
#[test]
fn test_register_scans_addons() {
let (_tmp, loader) = make_test_env();
let errors = loader.register().unwrap();
assert!(errors.is_empty(), "errors: {:?}", errors);
assert_eq!(loader.count(), 2);
assert!(loader.exists("operate"));
assert!(loader.exists("cashier"));
}
#[test]
fn test_register_returns_errors_for_malformed() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
let malformed_dir = addons_path.join("malformed");
fs::create_dir_all(&malformed_dir).unwrap();
fs::write(malformed_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
let loader = AddonLoader::new(&addons_path);
let errors = loader.register().unwrap();
assert!(!errors.is_empty());
assert_eq!(loader.count(), 0);
}
#[test]
fn test_register_nonexistent_path() {
let loader = AddonLoader::new("/nonexistent/path/12345");
let result = loader.register();
assert!(result.is_err());
}
#[test]
fn test_parse_route_valid() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let route = loader.parse_route("/addons/operate/Order/index").unwrap();
assert_eq!(route.addon, "operate");
assert_eq!(route.controller, "Order");
assert_eq!(route.action, "index");
assert!(route.controller_file.is_some());
}
#[test]
fn test_parse_route_dotted_controller() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let route = loader
.parse_route("/addons/operate/admin.Order/index")
.unwrap();
assert_eq!(route.controller, "admin.Order");
assert!(route.controller_file.is_some());
}
#[test]
fn test_parse_route_disabled_addon() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let result = loader.parse_route("/addons/cashier/Order/index");
assert!(result.is_err());
match result.unwrap_err() {
AddonLoaderError::AddonDisabled(name) => assert_eq!(name, "cashier"),
other => panic!("expected AddonDisabled, got {:?}", other),
}
}
#[test]
fn test_parse_route_not_found_addon() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let result = loader.parse_route("/addons/ghost/Order/index");
assert!(result.is_err());
}
#[test]
fn test_parse_route_controller_not_found() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let result = loader.parse_route("/addons/operate/Ghost/index");
assert!(result.is_err());
}
#[test]
fn test_names_sorted() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let names = loader.names();
assert_eq!(names, vec!["cashier", "operate"]);
}
#[test]
fn test_all_addons_sorted() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let all = loader.all_addons();
assert_eq!(all.len(), 2);
assert_eq!(all[0].name, "cashier");
assert_eq!(all[1].name, "operate");
}
#[test]
fn test_enabled_addons_filtered() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let enabled = loader.enabled_addons();
assert_eq!(enabled.len(), 1);
assert_eq!(enabled[0].name, "operate");
}
#[test]
fn test_disabled_addons_filtered() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let disabled = loader.disabled_addons();
assert_eq!(disabled.len(), 1);
assert_eq!(disabled[0].name, "cashier");
}
#[test]
fn test_exists() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
assert!(loader.exists("operate"));
assert!(loader.exists("cashier"));
assert!(!loader.exists("ghost"));
}
#[test]
fn test_is_enabled() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
assert!(loader.is_enabled("operate").unwrap());
assert!(!loader.is_enabled("cashier").unwrap());
assert!(loader.is_enabled("ghost").is_err());
}
#[test]
fn test_get_manifest() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let manifest = loader.get_manifest("operate").unwrap();
assert_eq!(manifest.name, "operate");
assert_eq!(manifest.title, "运营管理");
}
#[test]
fn test_get_manifest_not_found() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let result = loader.get_manifest("ghost");
assert!(result.is_err());
}
#[test]
fn test_count() {
let (_tmp, loader) = make_test_env();
assert_eq!(loader.count(), 0);
loader.register().unwrap();
assert_eq!(loader.count(), 2);
}
#[test]
fn test_resolve_class() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let path = loader.resolve_class("addons\\operate\\Plugin").unwrap();
assert!(path.is_some());
}
#[test]
fn test_resolve_class_non_addons() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let path = loader.resolve_class("app\\Home").unwrap();
assert!(path.is_none());
}
#[test]
fn test_resolve_controller() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let path = loader.resolve_controller("operate", "Order").unwrap();
assert!(path.is_some());
}
#[test]
fn test_resolve_controller_multilevel() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let path = loader.resolve_controller("operate", "admin.Order").unwrap();
assert!(path.is_some());
}
#[test]
fn test_resolve_plugin() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let path = loader.resolve_plugin("operate").unwrap();
assert!(path.is_some());
}
#[test]
fn test_registry_shared_via_arc() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let registry = loader.registry();
assert_eq!(registry.count(), 2);
assert!(registry.exists("operate"));
}
#[test]
fn test_autoload_accessor() {
let (_tmp, loader) = make_test_env();
loader.register().unwrap();
let autoload = loader.autoload();
assert_eq!(autoload.addons_path(), loader.addons_path());
}
}