1use std::path::{Path, PathBuf};
40use std::sync::Arc;
41
42use crate::autoload::AddonAutoload;
43use crate::error::{AddonLoaderError, AddonLoaderResult};
44use crate::manifest::AddonManifest;
45use crate::registry::AddonRegistry;
46
47#[derive(Debug)]
74pub struct AddonLoader {
75 addons_path: PathBuf,
77 registry: Arc<AddonRegistry>,
79 autoload: AddonAutoload,
81}
82
83impl AddonLoader {
84 pub fn new(addons_path: impl Into<PathBuf>) -> Self {
88 let addons_path = addons_path.into();
89 let autoload = AddonAutoload::new(&addons_path);
90 Self {
91 addons_path,
92 registry: Arc::new(AddonRegistry::new()),
93 autoload,
94 }
95 }
96
97 pub fn addons_path(&self) -> &Path {
99 &self.addons_path
100 }
101
102 pub fn registry(&self) -> &AddonRegistry {
104 &self.registry
105 }
106
107 pub fn autoload(&self) -> &AddonAutoload {
109 &self.autoload
110 }
111
112 pub fn register(&self) -> AddonLoaderResult<Vec<AddonLoaderError>> {
127 self.registry.load_from_directory(&self.addons_path)
128 }
129
130 pub fn parse_route(&self, url: &str) -> AddonLoaderResult<crate::route::AddonRoute> {
139 crate::route::parse_route(url, &self.registry, &self.autoload)
140 }
141
142 pub fn names(&self) -> Vec<String> {
144 self.registry.names()
145 }
146
147 pub fn all_addons(&self) -> Vec<AddonManifest> {
149 self.registry.all()
150 }
151
152 pub fn enabled_addons(&self) -> Vec<AddonManifest> {
154 self.registry.enabled_addons()
155 }
156
157 pub fn disabled_addons(&self) -> Vec<AddonManifest> {
159 self.registry.disabled_addons()
160 }
161
162 pub fn exists(&self, name: &str) -> bool {
164 self.registry.exists(name)
165 }
166
167 pub fn is_enabled(&self, name: &str) -> AddonLoaderResult<bool> {
169 self.registry.is_enabled(name)
170 }
171
172 pub fn get_manifest(&self, name: &str) -> AddonLoaderResult<AddonManifest> {
174 self.registry.get(name)
175 }
176
177 pub fn count(&self) -> usize {
179 self.registry.count()
180 }
181
182 pub fn resolve_class(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
184 self.autoload.resolve(class)
185 }
186
187 pub fn resolve_controller(
189 &self,
190 addon: &str,
191 controller: &str,
192 ) -> AddonLoaderResult<Option<PathBuf>> {
193 self.autoload.resolve_controller(addon, controller)
194 }
195
196 pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
198 self.autoload.resolve_plugin(addon)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use std::fs;
206
207 fn make_test_env() -> (tempfile::TempDir, AddonLoader) {
208 let tmp = tempfile::tempdir().expect("create tempdir");
209 let addons_path = tmp.path().join("addons");
210
211 let operate_dir = addons_path.join("operate");
213 fs::create_dir_all(&operate_dir).unwrap();
214 fs::write(
215 operate_dir.join("Plugin.php"),
216 r#"
217public $info = [
218 'name' => 'operate',
219 'title' => '运营管理',
220 'status' => 1,
221];
222"#,
223 )
224 .unwrap();
225
226 let controller_dir = operate_dir.join("controller");
228 fs::create_dir_all(&controller_dir).unwrap();
229 fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
230
231 let admin_dir = controller_dir.join("admin");
233 fs::create_dir_all(&admin_dir).unwrap();
234 fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
235
236 let cashier_dir = addons_path.join("cashier");
238 fs::create_dir_all(&cashier_dir).unwrap();
239 fs::write(
240 cashier_dir.join("Plugin.php"),
241 r#"
242public $info = [
243 'name' => 'cashier',
244 'status' => 0,
245];
246"#,
247 )
248 .unwrap();
249
250 let loader = AddonLoader::new(&addons_path);
251 (tmp, loader)
252 }
253
254 #[test]
255 fn test_new_loader() {
256 let loader = AddonLoader::new("/addons");
257 assert_eq!(loader.addons_path(), Path::new("/addons"));
258 assert_eq!(loader.count(), 0);
259 }
260
261 #[test]
262 fn test_register_scans_addons() {
263 let (_tmp, loader) = make_test_env();
264 let errors = loader.register().unwrap();
265 assert!(errors.is_empty(), "errors: {:?}", errors);
266 assert_eq!(loader.count(), 2);
267 assert!(loader.exists("operate"));
268 assert!(loader.exists("cashier"));
269 }
270
271 #[test]
272 fn test_register_returns_errors_for_malformed() {
273 let tmp = tempfile::tempdir().unwrap();
274 let addons_path = tmp.path().join("addons");
275 let malformed_dir = addons_path.join("malformed");
276 fs::create_dir_all(&malformed_dir).unwrap();
277 fs::write(malformed_dir.join("Plugin.php"), "<?php class Plugin {}").unwrap();
279
280 let loader = AddonLoader::new(&addons_path);
281 let errors = loader.register().unwrap();
282 assert!(!errors.is_empty());
283 assert_eq!(loader.count(), 0);
284 }
285
286 #[test]
287 fn test_register_nonexistent_path() {
288 let loader = AddonLoader::new("/nonexistent/path/12345");
289 let result = loader.register();
290 assert!(result.is_err());
291 }
292
293 #[test]
294 fn test_parse_route_valid() {
295 let (_tmp, loader) = make_test_env();
296 loader.register().unwrap();
297
298 let route = loader.parse_route("/addons/operate/Order/index").unwrap();
299 assert_eq!(route.addon, "operate");
300 assert_eq!(route.controller, "Order");
301 assert_eq!(route.action, "index");
302 assert!(route.controller_file.is_some());
303 }
304
305 #[test]
306 fn test_parse_route_dotted_controller() {
307 let (_tmp, loader) = make_test_env();
308 loader.register().unwrap();
309
310 let route = loader
311 .parse_route("/addons/operate/admin.Order/index")
312 .unwrap();
313 assert_eq!(route.controller, "admin.Order");
314 assert!(route.controller_file.is_some());
315 }
316
317 #[test]
318 fn test_parse_route_disabled_addon() {
319 let (_tmp, loader) = make_test_env();
320 loader.register().unwrap();
321
322 let result = loader.parse_route("/addons/cashier/Order/index");
323 assert!(result.is_err());
324 match result.unwrap_err() {
325 AddonLoaderError::AddonDisabled(name) => assert_eq!(name, "cashier"),
326 other => panic!("expected AddonDisabled, got {:?}", other),
327 }
328 }
329
330 #[test]
331 fn test_parse_route_not_found_addon() {
332 let (_tmp, loader) = make_test_env();
333 loader.register().unwrap();
334
335 let result = loader.parse_route("/addons/ghost/Order/index");
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn test_parse_route_controller_not_found() {
341 let (_tmp, loader) = make_test_env();
342 loader.register().unwrap();
343
344 let result = loader.parse_route("/addons/operate/Ghost/index");
345 assert!(result.is_err());
346 }
347
348 #[test]
349 fn test_names_sorted() {
350 let (_tmp, loader) = make_test_env();
351 loader.register().unwrap();
352
353 let names = loader.names();
354 assert_eq!(names, vec!["cashier", "operate"]);
355 }
356
357 #[test]
358 fn test_all_addons_sorted() {
359 let (_tmp, loader) = make_test_env();
360 loader.register().unwrap();
361
362 let all = loader.all_addons();
363 assert_eq!(all.len(), 2);
364 assert_eq!(all[0].name, "cashier");
365 assert_eq!(all[1].name, "operate");
366 }
367
368 #[test]
369 fn test_enabled_addons_filtered() {
370 let (_tmp, loader) = make_test_env();
371 loader.register().unwrap();
372
373 let enabled = loader.enabled_addons();
374 assert_eq!(enabled.len(), 1);
375 assert_eq!(enabled[0].name, "operate");
376 }
377
378 #[test]
379 fn test_disabled_addons_filtered() {
380 let (_tmp, loader) = make_test_env();
381 loader.register().unwrap();
382
383 let disabled = loader.disabled_addons();
384 assert_eq!(disabled.len(), 1);
385 assert_eq!(disabled[0].name, "cashier");
386 }
387
388 #[test]
389 fn test_exists() {
390 let (_tmp, loader) = make_test_env();
391 loader.register().unwrap();
392
393 assert!(loader.exists("operate"));
394 assert!(loader.exists("cashier"));
395 assert!(!loader.exists("ghost"));
396 }
397
398 #[test]
399 fn test_is_enabled() {
400 let (_tmp, loader) = make_test_env();
401 loader.register().unwrap();
402
403 assert!(loader.is_enabled("operate").unwrap());
404 assert!(!loader.is_enabled("cashier").unwrap());
405 assert!(loader.is_enabled("ghost").is_err());
406 }
407
408 #[test]
409 fn test_get_manifest() {
410 let (_tmp, loader) = make_test_env();
411 loader.register().unwrap();
412
413 let manifest = loader.get_manifest("operate").unwrap();
414 assert_eq!(manifest.name, "operate");
415 assert_eq!(manifest.title, "运营管理");
416 }
417
418 #[test]
419 fn test_get_manifest_not_found() {
420 let (_tmp, loader) = make_test_env();
421 loader.register().unwrap();
422
423 let result = loader.get_manifest("ghost");
424 assert!(result.is_err());
425 }
426
427 #[test]
428 fn test_count() {
429 let (_tmp, loader) = make_test_env();
430 assert_eq!(loader.count(), 0);
431 loader.register().unwrap();
432 assert_eq!(loader.count(), 2);
433 }
434
435 #[test]
436 fn test_resolve_class() {
437 let (_tmp, loader) = make_test_env();
438 loader.register().unwrap();
439
440 let path = loader.resolve_class("addons\\operate\\Plugin").unwrap();
441 assert!(path.is_some());
442 }
443
444 #[test]
445 fn test_resolve_class_non_addons() {
446 let (_tmp, loader) = make_test_env();
447 loader.register().unwrap();
448
449 let path = loader.resolve_class("app\\Home").unwrap();
450 assert!(path.is_none());
451 }
452
453 #[test]
454 fn test_resolve_controller() {
455 let (_tmp, loader) = make_test_env();
456 loader.register().unwrap();
457
458 let path = loader.resolve_controller("operate", "Order").unwrap();
459 assert!(path.is_some());
460 }
461
462 #[test]
463 fn test_resolve_controller_multilevel() {
464 let (_tmp, loader) = make_test_env();
465 loader.register().unwrap();
466
467 let path = loader.resolve_controller("operate", "admin.Order").unwrap();
468 assert!(path.is_some());
469 }
470
471 #[test]
472 fn test_resolve_plugin() {
473 let (_tmp, loader) = make_test_env();
474 loader.register().unwrap();
475
476 let path = loader.resolve_plugin("operate").unwrap();
477 assert!(path.is_some());
478 }
479
480 #[test]
481 fn test_registry_shared_via_arc() {
482 let (_tmp, loader) = make_test_env();
483 loader.register().unwrap();
484
485 let registry = loader.registry();
487 assert_eq!(registry.count(), 2);
488 assert!(registry.exists("operate"));
489 }
490
491 #[test]
492 fn test_autoload_accessor() {
493 let (_tmp, loader) = make_test_env();
494 loader.register().unwrap();
495
496 let autoload = loader.autoload();
497 assert_eq!(autoload.addons_path(), loader.addons_path());
498 }
499}