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