sz_rust_addons_loader/
autoload.rs1use std::path::{Path, PathBuf};
54
55use crate::error::{AddonLoaderError, AddonLoaderResult};
56
57#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct AddonAutoload {
66 addons_path: PathBuf,
68}
69
70impl AddonAutoload {
71 pub fn new(addons_path: impl Into<PathBuf>) -> Self {
75 Self {
76 addons_path: addons_path.into(),
77 }
78 }
79
80 pub fn addons_path(&self) -> &Path {
82 &self.addons_path
83 }
84
85 #[tracing::instrument(skip(self))]
105 pub fn resolve(&self, class: &str) -> AddonLoaderResult<Option<PathBuf>> {
106 let class = class.trim_start_matches('\\');
107
108 if !class.starts_with("addons\\") {
110 return Ok(None);
111 }
112
113 let stripped = &class["addons\\".len()..];
117
118 let (path_part, class_part) = if let Some(pos) = stripped.rfind('\\') {
120 let path = stripped[..pos].replace('\\', "/");
122 let class_name = &stripped[pos + 1..];
123 (format!("{}/", path), class_name.to_string())
124 } else {
125 (String::new(), stripped.to_string())
127 };
128
129 let class_file = format!("{}.php", class_part.replace('_', "/"));
132
133 let file_path = self
135 .addons_path
136 .join(format!("{}{}", path_part, class_file));
137
138 if file_path.exists() {
139 Ok(Some(file_path))
140 } else {
141 Ok(None)
142 }
143 }
144
145 #[tracing::instrument(skip(self))]
166 pub fn resolve_controller(
167 &self,
168 addon: &str,
169 controller: &str,
170 ) -> AddonLoaderResult<Option<PathBuf>> {
171 let controller_class = parse_dotted_controller(controller);
172 let full_class = format!("addons\\{}\\controller\\{}", addon, controller_class);
173 self.resolve(&full_class)
174 }
175
176 #[tracing::instrument(skip(self))]
182 pub fn resolve_plugin(&self, addon: &str) -> AddonLoaderResult<Option<PathBuf>> {
183 let full_class = format!("addons\\{}\\Plugin", addon);
184 self.resolve(&full_class)
185 }
186
187 #[tracing::instrument(skip(self))]
191 pub fn resolve_strict(&self, class: &str) -> AddonLoaderResult<PathBuf> {
192 let class = class.trim_start_matches('\\');
193
194 if !class.starts_with("addons\\") {
195 return Err(AddonLoaderError::AutoloadMiss {
196 class: class.to_string(),
197 });
198 }
199
200 let stripped = &class["addons\\".len()..];
201 let (path_part, class_part) = if let Some(pos) = stripped.rfind('\\') {
202 let path = stripped[..pos].replace('\\', "/");
203 let class_name = &stripped[pos + 1..];
204 (format!("{}/", path), class_name.to_string())
205 } else {
206 (String::new(), stripped.to_string())
207 };
208
209 let class_file = format!("{}.php", class_part.replace('_', "/"));
210 let file_path = self
211 .addons_path
212 .join(format!("{}{}", path_part, class_file));
213 Ok(file_path)
214 }
215}
216
217fn parse_dotted_controller(controller: &str) -> String {
236 if !controller.contains('.') {
237 return controller.to_string();
238 }
239
240 let mut parts: Vec<&str> = controller.split('.').collect();
241 if parts.len() == 1 {
242 return controller.to_string();
243 }
244
245 let last = parts
247 .pop()
248 .expect("已通过 contains('.') 与 len 检查保证 parts 非空");
249 let last_studly = studly_case(last);
250
251 parts.push(&last_studly);
253 parts.join("\\")
254}
255
256fn studly_case(s: &str) -> String {
264 s.split('_')
265 .map(|part| {
266 let mut chars = part.chars();
267 match chars.next() {
268 None => String::new(),
269 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
270 }
271 })
272 .collect()
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use std::fs;
279
280 fn make_test_addons_dir() -> tempfile::TempDir {
282 let tmp = tempfile::tempdir().expect("create tempdir");
283 let addons_path = tmp.path().join("addons");
284
285 let operate_dir = addons_path.join("operate");
287 fs::create_dir_all(&operate_dir).expect("create operate dir");
288 fs::write(operate_dir.join("Plugin.php"), "<?php // stub").expect("write Plugin.php");
289
290 let controller_dir = operate_dir.join("controller");
292 fs::create_dir_all(&controller_dir).expect("create controller dir");
293 fs::write(controller_dir.join("Order.php"), "<?php // stub").expect("write Order.php");
294
295 let admin_dir = controller_dir.join("admin");
297 fs::create_dir_all(&admin_dir).expect("create admin dir");
298 fs::write(admin_dir.join("Order.php"), "<?php // stub").expect("write admin/Order.php");
299
300 let model_dir = operate_dir.join("model");
302 fs::create_dir_all(&model_dir).expect("create model dir");
303 fs::write(model_dir.join("Customer.php"), "<?php // stub").expect("write Customer.php");
304
305 tmp
310 }
311
312 #[test]
313 fn test_new_autoload() {
314 let loader = AddonAutoload::new("/addons");
315 assert_eq!(loader.addons_path(), Path::new("/addons"));
316 }
317
318 #[test]
319 fn test_resolve_plugin_class() {
320 let tmp = make_test_addons_dir();
321 let addons_path = tmp.path().join("addons");
322 let loader = AddonAutoload::new(&addons_path);
323
324 let result = loader.resolve("addons\\operate\\Plugin").unwrap();
325 assert!(result.is_some());
326 let path = result.unwrap();
327 assert_eq!(path, addons_path.join("operate").join("Plugin.php"));
328 }
329
330 #[test]
331 fn test_resolve_controller_class() {
332 let tmp = make_test_addons_dir();
333 let addons_path = tmp.path().join("addons");
334 let loader = AddonAutoload::new(&addons_path);
335
336 let result = loader
337 .resolve("addons\\operate\\controller\\Order")
338 .unwrap();
339 assert!(result.is_some());
340 let path = result.unwrap();
341 assert_eq!(
342 path,
343 addons_path
344 .join("operate")
345 .join("controller")
346 .join("Order.php")
347 );
348 }
349
350 #[test]
351 fn test_resolve_multilevel_controller_class() {
352 let tmp = make_test_addons_dir();
353 let addons_path = tmp.path().join("addons");
354 let loader = AddonAutoload::new(&addons_path);
355
356 let result = loader
357 .resolve("addons\\operate\\controller\\admin\\Order")
358 .unwrap();
359 assert!(result.is_some());
360 let path = result.unwrap();
361 assert_eq!(
362 path,
363 addons_path
364 .join("operate")
365 .join("controller")
366 .join("admin")
367 .join("Order.php")
368 );
369 }
370
371 #[test]
372 fn test_resolve_model_class() {
373 let tmp = make_test_addons_dir();
374 let addons_path = tmp.path().join("addons");
375 let loader = AddonAutoload::new(&addons_path);
376
377 let result = loader.resolve("addons\\operate\\model\\Customer").unwrap();
378 assert!(result.is_some());
379 let path = result.unwrap();
380 assert_eq!(
381 path,
382 addons_path
383 .join("operate")
384 .join("model")
385 .join("Customer.php")
386 );
387 }
388
389 #[test]
390 fn test_resolve_non_addons_namespace_returns_none() {
391 let tmp = make_test_addons_dir();
392 let addons_path = tmp.path().join("addons");
393 let loader = AddonAutoload::new(&addons_path);
394
395 let result = loader.resolve("app\\controller\\Home").unwrap();
396 assert!(result.is_none());
397 }
398
399 #[test]
400 fn test_resolve_nonexistent_file_returns_none() {
401 let tmp = make_test_addons_dir();
402 let addons_path = tmp.path().join("addons");
403 let loader = AddonAutoload::new(&addons_path);
404
405 let result = loader.resolve("addons\\operate\\NonExistent").unwrap();
406 assert!(result.is_none());
407 }
408
409 #[test]
410 fn test_resolve_leading_backslash_stripped() {
411 let tmp = make_test_addons_dir();
412 let addons_path = tmp.path().join("addons");
413 let loader = AddonAutoload::new(&addons_path);
414
415 let result = loader.resolve("\\addons\\operate\\Plugin").unwrap();
416 assert!(result.is_some());
417 }
418
419 #[test]
420 fn test_resolve_controller_helper() {
421 let tmp = make_test_addons_dir();
422 let addons_path = tmp.path().join("addons");
423 let loader = AddonAutoload::new(&addons_path);
424
425 let result = loader.resolve_controller("operate", "Order").unwrap();
426 assert!(result.is_some());
427 let path = result.unwrap();
428 assert_eq!(
429 path,
430 addons_path
431 .join("operate")
432 .join("controller")
433 .join("Order.php")
434 );
435 }
436
437 #[test]
438 fn test_resolve_controller_multilevel_dotted() {
439 let tmp = make_test_addons_dir();
440 let addons_path = tmp.path().join("addons");
441 let loader = AddonAutoload::new(&addons_path);
442
443 let result = loader.resolve_controller("operate", "admin.Order").unwrap();
444 assert!(result.is_some());
445 let path = result.unwrap();
446 assert_eq!(
447 path,
448 addons_path
449 .join("operate")
450 .join("controller")
451 .join("admin")
452 .join("Order.php")
453 );
454 }
455
456 #[test]
457 fn test_resolve_controller_three_levels_dotted() {
458 let tmp = make_test_addons_dir();
459 let addons_path = tmp.path().join("addons");
460 let sub_dir = addons_path
462 .join("operate")
463 .join("controller")
464 .join("admin")
465 .join("sub");
466 fs::create_dir_all(&sub_dir).expect("create sub dir");
467 fs::write(sub_dir.join("Order.php"), "<?php // stub").expect("write sub/Order.php");
468
469 let loader = AddonAutoload::new(&addons_path);
470 let result = loader
471 .resolve_controller("operate", "admin.sub.Order")
472 .unwrap();
473 assert!(result.is_some());
474 let path = result.unwrap();
475 assert!(path.to_string_lossy().contains("admin"));
476 assert!(path.to_string_lossy().contains("sub"));
477 assert!(path.to_string_lossy().ends_with("Order.php"));
478 }
479
480 #[test]
481 fn test_resolve_plugin_helper() {
482 let tmp = make_test_addons_dir();
483 let addons_path = tmp.path().join("addons");
484 let loader = AddonAutoload::new(&addons_path);
485
486 let result = loader.resolve_plugin("operate").unwrap();
487 assert!(result.is_some());
488 let path = result.unwrap();
489 assert_eq!(path, addons_path.join("operate").join("Plugin.php"));
490 }
491
492 #[test]
493 fn test_resolve_strict_addons_class() {
494 let loader = AddonAutoload::new("/addons");
495 let path = loader.resolve_strict("addons\\operate\\Plugin").unwrap();
496 assert_eq!(path, PathBuf::from("/addons/operate/Plugin.php"));
497 }
498
499 #[test]
500 fn test_resolve_strict_controller_class() {
501 let loader = AddonAutoload::new("/addons");
502 let path = loader
503 .resolve_strict("addons\\operate\\controller\\admin\\Order")
504 .unwrap();
505 assert_eq!(
506 path,
507 PathBuf::from("/addons/operate/controller/admin/Order.php")
508 );
509 }
510
511 #[test]
512 fn test_resolve_strict_non_addons_returns_error() {
513 let loader = AddonAutoload::new("/addons");
514 let result = loader.resolve_strict("app\\Home");
515 assert!(result.is_err());
516 match result.unwrap_err() {
517 AddonLoaderError::AutoloadMiss { class } => {
518 assert_eq!(class, "app\\Home");
519 }
520 other => panic!("expected AutoloadMiss, got {:?}", other),
521 }
522 }
523
524 #[test]
525 fn test_parse_dotted_controller_simple() {
526 assert_eq!(parse_dotted_controller("Order"), "Order");
527 }
528
529 #[test]
530 fn test_parse_dotted_controller_two_levels() {
531 assert_eq!(parse_dotted_controller("admin.Order"), "admin\\Order");
532 }
533
534 #[test]
535 fn test_parse_dotted_controller_three_levels() {
536 assert_eq!(
537 parse_dotted_controller("admin.sub.Order"),
538 "admin\\sub\\Order"
539 );
540 }
541
542 #[test]
543 fn test_parse_dotted_controller_studly_conversion() {
544 assert_eq!(
546 parse_dotted_controller("admin.user_order"),
547 "admin\\UserOrder"
548 );
549 }
550
551 #[test]
552 fn test_parse_dotted_controller_no_dot_passthrough() {
553 assert_eq!(parse_dotted_controller("user_order"), "user_order");
554 }
556
557 #[test]
558 fn test_studly_case_basic() {
559 assert_eq!(studly_case("order"), "Order");
560 }
561
562 #[test]
563 fn test_studly_case_with_underscore() {
564 assert_eq!(studly_case("user_order"), "UserOrder");
565 }
566
567 #[test]
568 fn test_studly_case_already_studly() {
569 assert_eq!(studly_case("Order"), "Order");
570 }
571
572 #[test]
573 fn test_studly_case_empty() {
574 assert_eq!(studly_case(""), "");
575 }
576
577 #[test]
578 fn test_studly_case_multiple_underscores() {
579 assert_eq!(studly_case("a_b_c"), "ABC");
580 }
581
582 #[test]
583 fn test_clone_eq() {
584 let l1 = AddonAutoload::new("/addons");
585 let l2 = l1.clone();
586 assert_eq!(l1, l2);
587 }
588
589 #[test]
590 fn test_resolve_with_trailing_backslash_in_class() {
591 let loader = AddonAutoload::new("/addons");
593 let result = loader.resolve_strict("addons\\operate\\Plugin\\").unwrap();
594 assert!(result.to_string_lossy().ends_with(".php"));
596 }
597}