#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub mod autoload;
pub mod error;
pub mod loader;
pub mod manifest;
pub mod registry;
pub mod resource;
pub mod route;
pub use autoload::AddonAutoload;
pub use error::{AddonLoaderError, AddonLoaderResult};
pub use loader::AddonLoader;
pub use manifest::AddonManifest;
pub use registry::AddonRegistry;
pub use resource::{
build_resource_routes, HttpMethod, ResourceAction, ResourceOptions, ResourceRoute,
ResourceRouteEntry,
};
pub use route::AddonRoute;
#[cfg(test)]
mod integration_tests {
use super::*;
use std::fs;
#[test]
fn test_end_to_end_lifecycle() {
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#"<?php
namespace addons\operate;
use think\Addons;
class Plugin extends Addons {
public $info = [
'name' => 'operate',
'title' => '运营管理',
'identifier' => 'operate.addon',
'icon' => 'fa-cog',
'author' => 'sz',
'version' => '1.0.0',
'admin' => 'operate/index/index',
'status' => 1,
];
public function install() {}
public function uninstall() {}
}
"#,
)
.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 disabled_dir = addons_path.join("disabled");
fs::create_dir_all(&disabled_dir).unwrap();
fs::write(
disabled_dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'disabled',
'status' => 0,
];
"#,
)
.unwrap();
let loader = AddonLoader::new(&addons_path);
assert_eq!(loader.count(), 0);
let errors = loader.register().unwrap();
assert!(errors.is_empty(), "register errors: {:?}", errors);
assert_eq!(loader.count(), 2);
assert!(loader.is_enabled("operate").unwrap());
assert!(!loader.is_enabled("disabled").unwrap());
let manifest = loader.get_manifest("operate").unwrap();
assert_eq!(manifest.name, "operate");
assert_eq!(manifest.title, "运营管理");
assert_eq!(manifest.version, "1.0.0");
assert!(manifest.is_enabled());
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_eq!(route.controller_class, "addons\\operate\\controller\\Order");
assert!(route.controller_file.is_some());
assert!(route.controller_file.unwrap().exists());
let route2 = loader
.parse_route("/addons/operate/admin.Order/index")
.unwrap();
assert_eq!(route2.controller, "admin.Order");
assert_eq!(
route2.controller_class,
"addons\\operate\\controller\\admin\\Order"
);
assert!(route2.controller_file.is_some());
let result = loader.parse_route("/addons/disabled/Order/index");
assert!(matches!(result, Err(AddonLoaderError::AddonDisabled(_))));
let result = loader.parse_route("/addons/ghost/Order/index");
assert!(matches!(result, Err(AddonLoaderError::AddonNotFound(_))));
}
#[test]
fn test_mixed_enabled_disabled_addons() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
for (name, status) in [
("enabled1", 1),
("enabled2", 1),
("disabled1", 0),
("disabled2", 0),
] {
let dir = addons_path.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("Plugin.php"),
format!(
r#"
public $info = [
'name' => '{}',
'status' => {},
];
"#,
name, status
),
)
.unwrap();
}
let loader = AddonLoader::new(&addons_path);
loader.register().unwrap();
assert_eq!(loader.count(), 4);
assert_eq!(loader.enabled_addons().len(), 2);
assert_eq!(loader.disabled_addons().len(), 2);
assert_eq!(
loader.names(),
vec!["disabled1", "disabled2", "enabled1", "enabled2"]
);
}
#[test]
fn test_dynamic_status_toggle() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
let dir = addons_path.join("operate");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'operate',
'status' => 1,
];
"#,
)
.unwrap();
let loader = AddonLoader::new(&addons_path);
loader.register().unwrap();
assert!(loader.is_enabled("operate").unwrap());
loader.registry().set_enabled("operate", false).unwrap();
assert!(!loader.is_enabled("operate").unwrap());
loader.registry().set_enabled("operate", true).unwrap();
assert!(loader.is_enabled("operate").unwrap());
}
#[test]
fn test_autoload_registry_collaboration() {
let tmp = tempfile::tempdir().unwrap();
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"), "<?php // stub").unwrap();
fs::write(
operate_dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'operate',
'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 loader = AddonLoader::new(&addons_path);
loader.register().unwrap();
let path = loader
.resolve_class("addons\\operate\\controller\\Order")
.unwrap();
assert!(path.is_some());
assert!(path.unwrap().exists());
let plugin_path = loader.resolve_plugin("operate").unwrap();
assert!(plugin_path.is_some());
assert!(plugin_path.unwrap().exists());
}
#[test]
fn test_multiple_addons_scan() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
for name in &["operate", "cashier", "food", "erp"] {
let dir = addons_path.join(name);
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("Plugin.php"),
format!(
r#"
public $info = [
'name' => '{}',
'status' => 1,
];
"#,
name
),
)
.unwrap();
}
let loader = AddonLoader::new(&addons_path);
let errors = loader.register().unwrap();
assert!(errors.is_empty());
assert_eq!(loader.count(), 4);
assert_eq!(loader.names(), vec!["cashier", "erp", "food", "operate"]);
}
#[test]
fn test_partial_failure_continues_scan() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
let good_dir = addons_path.join("good");
fs::create_dir_all(&good_dir).unwrap();
fs::write(
good_dir.join("Plugin.php"),
r#"
public $info = [
'name' => 'good',
'status' => 1,
];
"#,
)
.unwrap();
let bad_dir = addons_path.join("bad");
fs::create_dir_all(&bad_dir).unwrap();
fs::write(bad_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(), 1);
assert!(loader.exists("good"));
assert!(!loader.exists("bad"));
}
#[test]
fn test_empty_addons_directory() {
let tmp = tempfile::tempdir().unwrap();
let addons_path = tmp.path().join("addons");
fs::create_dir_all(&addons_path).unwrap();
let loader = AddonLoader::new(&addons_path);
let errors = loader.register().unwrap();
assert!(errors.is_empty());
assert_eq!(loader.count(), 0);
}
}