use std::path::{Path, PathBuf};
use crate::error::{AddonLoaderError, AddonLoaderResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddonAutoload {
addons_path: PathBuf,
}
impl AddonAutoload {
pub fn new(addons_path: impl Into<PathBuf>) -> Self {
Self {
addons_path: addons_path.into(),
}
}
pub fn addons_path(&self) -> &Path {
&self.addons_path
}
#[tracing::instrument(skip(self))]
pub fn resolve(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
let class = class.trim_start_matches('\\');
if !class.starts_with("addons\\") {
return Ok(None);
}
let stripped = &class["addons\\".len()..];
let (path_part, class_part) = if let Some(pos) = stripped.rfind('\\') {
let path = stripped[..pos].replace('\\', "/");
let class_name = &stripped[pos + 1..];
(format!("{}/", path), class_name.to_string())
} else {
(String::new(), stripped.to_string())
};
let class_file = format!("{}.php", class_part.replace('_', "/"));
let file_path = self
.addons_path
.join(format!("{}{}", path_part, class_file));
if file_path.exists() {
Ok(Some(file_path))
} else {
Ok(None)
}
}
#[tracing::instrument(skip(self))]
pub fn resolve_controller(
&self,
addon: &str,
controller: &str,
) -> AddonLoaderResult<Option<PathBuf>> {
let controller_class = parse_dotted_controller(controller);
let full_class = format!("addons\\{}\\controller\\{}", addon, controller_class);
self.resolve(&full_class)
}
#[tracing::instrument(skip(self))]
pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
let full_class = format!("addons\\{}\\Plugin", addon);
self.resolve(&full_class)
}
#[tracing::instrument(skip(self))]
pub fn resolve_strict(&self, class: &str) -> AddonLoaderResult<PathBuf> {
let class = class.trim_start_matches('\\');
if !class.starts_with("addons\\") {
return Err(AddonLoaderError::AutoloadMiss {
class: class.to_string(),
});
}
let stripped = &class["addons\\".len()..];
let (path_part, class_part) = if let Some(pos) = stripped.rfind('\\') {
let path = stripped[..pos].replace('\\', "/");
let class_name = &stripped[pos + 1..];
(format!("{}/", path), class_name.to_string())
} else {
(String::new(), stripped.to_string())
};
let class_file = format!("{}.php", class_part.replace('_', "/"));
let file_path = self
.addons_path
.join(format!("{}{}", path_part, class_file));
Ok(file_path)
}
}
fn parse_dotted_controller(controller: &str) -> String {
if !controller.contains('.') {
return controller.to_string();
}
let mut parts: Vec<&str> = controller.split('.').collect();
if parts.len() == 1 {
return controller.to_string();
}
let last = parts
.pop()
.expect("已通过 contains('.') 与 len 检查保证 parts 非空");
let last_studly = studly_case(last);
parts.push(&last_studly);
parts.join("\\")
}
fn studly_case(s: &str) -> String {
s.split('_')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn make_test_addons_dir() -> tempfile::TempDir {
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).expect("create operate dir");
fs::write(operate_dir.join("Plugin.php"), "<?php // stub").expect("write Plugin.php");
let controller_dir = operate_dir.join("controller");
fs::create_dir_all(&controller_dir).expect("create controller dir");
fs::write(controller_dir.join("Order.php"), "<?php // stub").expect("write Order.php");
let admin_dir = controller_dir.join("admin");
fs::create_dir_all(&admin_dir).expect("create admin dir");
fs::write(admin_dir.join("Order.php"), "<?php // stub").expect("write admin/Order.php");
let model_dir = operate_dir.join("model");
fs::create_dir_all(&model_dir).expect("create model dir");
fs::write(model_dir.join("Customer.php"), "<?php // stub").expect("write Customer.php");
tmp
}
#[test]
fn test_new_autoload() {
let loader = AddonAutoload::new("/addons");
assert_eq!(loader.addons_path(), Path::new("/addons"));
}
#[test]
fn test_resolve_plugin_class() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve("addons\\operate\\Plugin").unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(path, addons_path.join("operate").join("Plugin.php"));
}
#[test]
fn test_resolve_controller_class() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader
.resolve("addons\\operate\\controller\\Order")
.unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(
path,
addons_path
.join("operate")
.join("controller")
.join("Order.php")
);
}
#[test]
fn test_resolve_multilevel_controller_class() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader
.resolve("addons\\operate\\controller\\admin\\Order")
.unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(
path,
addons_path
.join("operate")
.join("controller")
.join("admin")
.join("Order.php")
);
}
#[test]
fn test_resolve_model_class() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve("addons\\operate\\model\\Customer").unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(
path,
addons_path
.join("operate")
.join("model")
.join("Customer.php")
);
}
#[test]
fn test_resolve_non_addons_namespace_returns_none() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve("app\\controller\\Home").unwrap();
assert!(result.is_none());
}
#[test]
fn test_resolve_nonexistent_file_returns_none() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve("addons\\operate\\NonExistent").unwrap();
assert!(result.is_none());
}
#[test]
fn test_resolve_leading_backslash_stripped() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve("\\addons\\operate\\Plugin").unwrap();
assert!(result.is_some());
}
#[test]
fn test_resolve_controller_helper() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve_controller("operate", "Order").unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(
path,
addons_path
.join("operate")
.join("controller")
.join("Order.php")
);
}
#[test]
fn test_resolve_controller_multilevel_dotted() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve_controller("operate", "admin.Order").unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(
path,
addons_path
.join("operate")
.join("controller")
.join("admin")
.join("Order.php")
);
}
#[test]
fn test_resolve_controller_three_levels_dotted() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let sub_dir = addons_path
.join("operate")
.join("controller")
.join("admin")
.join("sub");
fs::create_dir_all(&sub_dir).expect("create sub dir");
fs::write(sub_dir.join("Order.php"), "<?php // stub").expect("write sub/Order.php");
let loader = AddonAutoload::new(&addons_path);
let result = loader
.resolve_controller("operate", "admin.sub.Order")
.unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert!(path.to_string_lossy().contains("admin"));
assert!(path.to_string_lossy().contains("sub"));
assert!(path.to_string_lossy().ends_with("Order.php"));
}
#[test]
fn test_resolve_plugin_helper() {
let tmp = make_test_addons_dir();
let addons_path = tmp.path().join("addons");
let loader = AddonAutoload::new(&addons_path);
let result = loader.resolve_plugin("operate").unwrap();
assert!(result.is_some());
let path = result.unwrap();
assert_eq!(path, addons_path.join("operate").join("Plugin.php"));
}
#[test]
fn test_resolve_strict_addons_class() {
let loader = AddonAutoload::new("/addons");
let path = loader.resolve_strict("addons\\operate\\Plugin").unwrap();
assert_eq!(path, PathBuf::from("/addons/operate/Plugin.php"));
}
#[test]
fn test_resolve_strict_controller_class() {
let loader = AddonAutoload::new("/addons");
let path = loader
.resolve_strict("addons\\operate\\controller\\admin\\Order")
.unwrap();
assert_eq!(
path,
PathBuf::from("/addons/operate/controller/admin/Order.php")
);
}
#[test]
fn test_resolve_strict_non_addons_returns_error() {
let loader = AddonAutoload::new("/addons");
let result = loader.resolve_strict("app\\Home");
assert!(result.is_err());
match result.unwrap_err() {
AddonLoaderError::AutoloadMiss { class } => {
assert_eq!(class, "app\\Home");
}
other => panic!("expected AutoloadMiss, got {:?}", other),
}
}
#[test]
fn test_parse_dotted_controller_simple() {
assert_eq!(parse_dotted_controller("Order"), "Order");
}
#[test]
fn test_parse_dotted_controller_two_levels() {
assert_eq!(parse_dotted_controller("admin.Order"), "admin\\Order");
}
#[test]
fn test_parse_dotted_controller_three_levels() {
assert_eq!(
parse_dotted_controller("admin.sub.Order"),
"admin\\sub\\Order"
);
}
#[test]
fn test_parse_dotted_controller_studly_conversion() {
assert_eq!(
parse_dotted_controller("admin.user_order"),
"admin\\UserOrder"
);
}
#[test]
fn test_parse_dotted_controller_no_dot_passthrough() {
assert_eq!(parse_dotted_controller("user_order"), "user_order");
}
#[test]
fn test_studly_case_basic() {
assert_eq!(studly_case("order"), "Order");
}
#[test]
fn test_studly_case_with_underscore() {
assert_eq!(studly_case("user_order"), "UserOrder");
}
#[test]
fn test_studly_case_already_studly() {
assert_eq!(studly_case("Order"), "Order");
}
#[test]
fn test_studly_case_empty() {
assert_eq!(studly_case(""), "");
}
#[test]
fn test_studly_case_multiple_underscores() {
assert_eq!(studly_case("a_b_c"), "ABC");
}
#[test]
fn test_clone_eq() {
let l1 = AddonAutoload::new("/addons");
let l2 = l1.clone();
assert_eq!(l1, l2);
}
#[test]
fn test_resolve_with_trailing_backslash_in_class() {
let loader = AddonAutoload::new("/addons");
let result = loader.resolve_strict("addons\\operate\\Plugin\\").unwrap();
assert!(result.to_string_lossy().ends_with(".php"));
}
}