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