Skip to main content

sz_rust_addons_loader/
route.rs

1//! 插件路由解析(Phase 10.3)
2//!
3//! ## PHP 对齐
4//!
5//! 对齐 PHP `think\addons\Route::execute($addon, $controller, $action)` 的 URL 解析逻辑:
6//!
7//! ```php
8//! // vendor/zzstudio/think-addons/src/addons/Route.php
9//! public static function execute($addon = null, $controller = null, $action = null)
10//! {
11//!     // 1. 参数校验
12//!     if (empty($addon) || empty($controller) || empty($action)) {
13//!         throw new HttpException(500, 'addon can not be empty');
14//!     }
15//!     // 2. 读取插件信息
16//!     $info = get_addons_info($addon);
17//!     if (!$info) {
18//!         throw new HttpException(404, 'addon %s not found');
19//!     }
20//!     if (!$info['status']) {
21//!         throw new HttpException(500, 'addon %s is disabled');
22//!     }
23//!     // 3. 解析控制器类
24//!     $class = get_addons_class($addon, 'controller', $controller);
25//!     // 4. 实例化并调用方法
26//! }
27//! ```
28//!
29//! ## 路由规则
30//!
31//! 对齐 PHP `Service::boot()` 注册的路由:
32//!
33//! ```php
34//! $route->rule("addons/:addon/[:controller]/[:action]", $execute);
35//! ```
36//!
37//! ## 多级控制器点号分隔
38//!
39//! 对齐 PHP `get_addons_class` 中 `.` 处理:
40//!
41//! - `admin.Order` → `admin\Order`(末段 studly 转大驼峰)
42//! - `admin.sub.Order` → `admin\sub\Order`
43
44use std::path::PathBuf;
45
46use crate::autoload::AddonAutoload;
47use crate::error::{AddonLoaderError, AddonLoaderResult};
48use crate::registry::AddonRegistry;
49
50/// 解析后的插件路由信息(Phase 10.3)
51///
52/// 对齐 PHP `Route::execute($addon, $controller, $action)` 的参数三元组。
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct AddonRoute {
55    /// 插件名(对齐 PHP `$addon`)
56    pub addon: String,
57    /// 控制器名(原始形式,可能含点号;对齐 PHP `$controller`)
58    pub controller: String,
59    /// 操作名(对齐 PHP `$action`)
60    pub action: String,
61    /// 控制器类名(解析后,对齐 PHP `get_addons_class` 返回值)
62    pub controller_class: String,
63    /// 控制器文件路径
64    pub controller_file: Option<PathBuf>,
65}
66
67impl AddonRoute {
68    /// 创建路由信息
69    pub fn new(
70        addon: impl Into<String>,
71        controller: impl Into<String>,
72        action: impl Into<String>,
73    ) -> Self {
74        let addon = addon.into();
75        let controller = controller.into();
76        let action = action.into();
77
78        // 对齐 PHP `get_addons_class($addon, 'controller', $controller)` 返回的类名
79        let controller_class = build_controller_class(&addon, &controller);
80
81        Self {
82            addon,
83            controller,
84            action,
85            controller_class,
86            controller_file: None,
87        }
88    }
89
90    /// 获取控制器文件路径(若已解析)
91    pub fn controller_file(&self) -> Option<&std::path::Path> {
92        self.controller_file.as_deref()
93    }
94}
95
96/// 解析插件路由(Phase 10.3 主入口)
97///
98/// 对齐 PHP `Route::execute($addon, $controller, $action)` 完整流程:
99///
100/// 1. 参数校验(对齐 PHP `empty($addon)` 等检查)
101/// 2. 插件存在性检查(对齐 PHP `get_addons_info($addon)` 返回 false)
102/// 3. 插件状态检查(对齐 PHP `!$info['status']` 抛 500)
103/// 4. 控制器类解析(对齐 PHP `get_addons_class($addon, 'controller', $controller)`)
104/// 5. 控制器文件存在性检查(对齐 PHP `class_exists($class)`)
105///
106/// ## 参数
107///
108/// - `url`:URL 路径(如 `/addons/operate/admin.Order/index`)
109/// - `registry`:插件注册中心
110/// - `autoload`:自动加载器
111///
112/// ## 错误
113///
114/// - `RouteParse`:URL 格式错误
115/// - `AddonNotFound`:插件不存在(对齐 PHP 404)
116/// - `AddonDisabled`:插件已禁用(对齐 PHP 500)
117/// - `ControllerNotFound`:控制器类或文件不存在(对齐 PHP 404)
118pub fn parse_route(
119    url: &str,
120    registry: &AddonRegistry,
121    autoload: &AddonAutoload,
122) -> AddonLoaderResult<AddonRoute> {
123    // 1. URL 解析
124    let (addon, controller, action) =
125        parse_url_segments(url).ok_or_else(|| AddonLoaderError::RouteParse {
126            url: url.to_string(),
127            reason: "URL must be /addons/<addon>/<controller>/<action>".to_string(),
128        })?;
129
130    // 2. 参数校验(对齐 PHP `empty($addon) || empty($controller) || empty($action)`)
131    if addon.is_empty() || controller.is_empty() || action.is_empty() {
132        return Err(AddonLoaderError::RouteParse {
133            url: url.to_string(),
134            reason: "addon, controller, action cannot be empty".to_string(),
135        });
136    }
137
138    // 3. 插件存在性检查(对齐 PHP `if (!$info)` → 404)
139    let manifest = registry.get(&addon)?;
140
141    // 4. 插件状态检查(对齐 PHP `if (!$info['status'])` → 500)
142    if !manifest.is_enabled() {
143        return Err(AddonLoaderError::AddonDisabled(addon));
144    }
145
146    // 5. 构建控制器类名
147    let mut route = AddonRoute::new(addon.clone(), controller, action);
148
149    // 6. 解析控制器文件路径(对齐 PHP `get_addons_class` + `class_exists` 检查)
150    let file_path = autoload.resolve_controller(&addon, &route.controller)?;
151
152    if file_path.is_none() {
153        // 对齐 PHP `HttpException(404, 'addon controller %s not found')`
154        return Err(AddonLoaderError::ControllerNotFound(
155            route.controller.clone(),
156        ));
157    }
158
159    route.controller_file = file_path;
160
161    Ok(route)
162}
163
164/// 解析 URL 段(对齐 PHP 路由规则 `addons/:addon/[:controller]/[:action]`)
165///
166/// ## 支持的 URL 格式
167///
168/// - `/addons/operate/Order/index` → `("operate", "Order", "index")`
169/// - `addons/operate/admin.Order/index` → `("operate", "admin.Order", "index")`
170/// - `/addons/operate/Order` → `("operate", "Order", "")`(action 缺失)
171/// - `/addons/operate` → `("operate", "", "")`(controller/action 缺失)
172///
173/// ## 返回
174///
175/// - `Some((addon, controller, action))`:URL 以 `addons/` 开头
176/// - `None`:URL 不以 `addons/` 开头
177fn parse_url_segments(url: &str) -> Option<(String, String, String)> {
178    // 去除前导斜杠
179    let url = url.trim_start_matches('/');
180
181    // 必须以 addons/ 开头
182    if !url.starts_with("addons/") {
183        return None;
184    }
185
186    let rest = &url["addons/".len()..];
187
188    // 拆分段
189    let parts: Vec<&str> = rest.split('/').collect();
190
191    let addon = parts.first().unwrap_or(&"").to_string();
192    let controller = parts.get(1).unwrap_or(&"").to_string();
193    let action = parts.get(2).unwrap_or(&"").to_string();
194
195    Some((addon, controller, action))
196}
197
198/// 构建控制器完整类名(对齐 PHP `get_addons_class($name, 'controller', $class)`)
199///
200/// ## PHP 对齐
201///
202/// PHP `get_addons_class` 中 `.` 处理逻辑:
203/// - 若 `$class` 含 `.`,按 `.` 拆分,末段 `Str::studly` 转大驼峰,再用 `\` 拼回
204/// - 单级时 `Str::studly(is_null($class) ? $name : $class)`
205/// - type='controller' 返回 `\addons\{name}\controller\{class}`
206/// - type='hook'(默认)返回 `\addons\{name}\Plugin`
207///
208/// ## 示例
209///
210/// - `build_controller_class("operate", "Order")` → `addons\operate\controller\Order`
211/// - `build_controller_class("operate", "admin.Order")` → `addons\operate\controller\admin\Order`
212fn build_controller_class(addon: &str, controller: &str) -> String {
213    let resolved = parse_dotted_controller(controller);
214    format!("addons\\{}\\controller\\{}", addon, resolved)
215}
216
217/// 解析多级控制器点号分隔(对齐 PHP `get_addons_class` 中 `.` 处理)
218///
219/// 直接复用 autoload 模块的同名函数。
220fn parse_dotted_controller(controller: &str) -> String {
221    if !controller.contains('.') {
222        return controller.to_string();
223    }
224
225    let mut parts: Vec<&str> = controller.split('.').collect();
226    if parts.len() == 1 {
227        return controller.to_string();
228    }
229
230    let last = parts.pop().unwrap();
231    let last_studly = studly_case(last);
232    parts.push(&last_studly);
233    parts.join("\\")
234}
235
236/// 下划线转大驼峰
237fn studly_case(s: &str) -> String {
238    s.split('_')
239        .map(|part| {
240            let mut chars = part.chars();
241            match chars.next() {
242                None => String::new(),
243                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
244            }
245        })
246        .collect()
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::fs;
253    use std::path::PathBuf;
254
255    fn make_test_env() -> (tempfile::TempDir, AddonRegistry, AddonAutoload) {
256        let tmp = tempfile::tempdir().expect("create tempdir");
257        let addons_path = tmp.path().join("addons");
258
259        // operate 插件
260        let operate_dir = addons_path.join("operate");
261        fs::create_dir_all(&operate_dir).unwrap();
262        fs::write(
263            operate_dir.join("Plugin.php"),
264            r#"
265public $info = [
266    'name' => 'operate',
267    'status' => 1,
268];
269"#,
270        )
271        .unwrap();
272
273        // operate/controller/Order.php
274        let controller_dir = operate_dir.join("controller");
275        fs::create_dir_all(&controller_dir).unwrap();
276        fs::write(controller_dir.join("Order.php"), "<?php // stub").unwrap();
277
278        // operate/controller/admin/Order.php
279        let admin_dir = controller_dir.join("admin");
280        fs::create_dir_all(&admin_dir).unwrap();
281        fs::write(admin_dir.join("Order.php"), "<?php // stub").unwrap();
282
283        // disabled 插件(status=0)
284        let disabled_dir = addons_path.join("disabled");
285        fs::create_dir_all(&disabled_dir).unwrap();
286        fs::write(
287            disabled_dir.join("Plugin.php"),
288            r#"
289public $info = [
290    'name' => 'disabled',
291    'status' => 0,
292];
293"#,
294        )
295        .unwrap();
296
297        // nonexistent 插件(无控制器)
298        let nonexistent_dir = addons_path.join("nonexistent");
299        fs::create_dir_all(&nonexistent_dir).unwrap();
300        fs::write(
301            nonexistent_dir.join("Plugin.php"),
302            r#"
303public $info = [
304    'name' => 'nonexistent',
305    'status' => 1,
306];
307"#,
308        )
309        .unwrap();
310
311        // 注册插件
312        let registry = AddonRegistry::new();
313        let _ = registry.load_from_directory(&addons_path).unwrap();
314
315        let autoload = AddonAutoload::new(&addons_path);
316
317        (tmp, registry, autoload)
318    }
319
320    #[test]
321    fn test_addon_route_new_simple() {
322        let route = AddonRoute::new("operate", "Order", "index");
323        assert_eq!(route.addon, "operate");
324        assert_eq!(route.controller, "Order");
325        assert_eq!(route.action, "index");
326        assert_eq!(route.controller_class, "addons\\operate\\controller\\Order");
327        assert!(route.controller_file.is_none());
328    }
329
330    #[test]
331    fn test_addon_route_new_dotted_controller() {
332        let route = AddonRoute::new("operate", "admin.Order", "index");
333        assert_eq!(route.controller, "admin.Order");
334        assert_eq!(
335            route.controller_class,
336            "addons\\operate\\controller\\admin\\Order"
337        );
338    }
339
340    #[test]
341    fn test_addon_route_new_three_level_dotted() {
342        let route = AddonRoute::new("operate", "admin.sub.Order", "index");
343        assert_eq!(
344            route.controller_class,
345            "addons\\operate\\controller\\admin\\sub\\Order"
346        );
347    }
348
349    #[test]
350    fn test_addon_route_controller_file_none() {
351        let route = AddonRoute::new("a", "b", "c");
352        assert!(route.controller_file().is_none());
353    }
354
355    #[test]
356    fn test_parse_url_segments_full_url() {
357        let result = parse_url_segments("/addons/operate/Order/index");
358        assert_eq!(
359            result,
360            Some((
361                "operate".to_string(),
362                "Order".to_string(),
363                "index".to_string()
364            ))
365        );
366    }
367
368    #[test]
369    fn test_parse_url_segments_no_leading_slash() {
370        let result = parse_url_segments("addons/operate/Order/index");
371        assert_eq!(
372            result,
373            Some((
374                "operate".to_string(),
375                "Order".to_string(),
376                "index".to_string()
377            ))
378        );
379    }
380
381    #[test]
382    fn test_parse_url_segments_missing_action() {
383        let result = parse_url_segments("/addons/operate/Order");
384        assert_eq!(
385            result,
386            Some(("operate".to_string(), "Order".to_string(), "".to_string()))
387        );
388    }
389
390    #[test]
391    fn test_parse_url_segments_missing_controller_and_action() {
392        let result = parse_url_segments("/addons/operate");
393        assert_eq!(
394            result,
395            Some(("operate".to_string(), "".to_string(), "".to_string()))
396        );
397    }
398
399    #[test]
400    fn test_parse_url_segments_dotted_controller() {
401        let result = parse_url_segments("/addons/operate/admin.Order/index");
402        assert_eq!(
403            result,
404            Some((
405                "operate".to_string(),
406                "admin.Order".to_string(),
407                "index".to_string()
408            ))
409        );
410    }
411
412    #[test]
413    fn test_parse_url_segments_non_addons_url() {
414        let result = parse_url_segments("/api/users");
415        assert_eq!(result, None);
416    }
417
418    #[test]
419    fn test_parse_url_segments_empty_url() {
420        let result = parse_url_segments("");
421        assert_eq!(result, None);
422    }
423
424    #[test]
425    fn test_parse_url_segments_only_addons() {
426        let result = parse_url_segments("/addons/");
427        assert_eq!(
428            result,
429            Some(("".to_string(), "".to_string(), "".to_string()))
430        );
431    }
432
433    #[test]
434    fn test_parse_url_segments_trailing_slash() {
435        let result = parse_url_segments("/addons/operate/Order/index/");
436        assert_eq!(
437            result,
438            Some((
439                "operate".to_string(),
440                "Order".to_string(),
441                "index".to_string()
442            ))
443        );
444    }
445
446    #[test]
447    fn test_build_controller_class_simple() {
448        let class = build_controller_class("operate", "Order");
449        assert_eq!(class, "addons\\operate\\controller\\Order");
450    }
451
452    #[test]
453    fn test_build_controller_class_dotted() {
454        let class = build_controller_class("operate", "admin.Order");
455        assert_eq!(class, "addons\\operate\\controller\\admin\\Order");
456    }
457
458    #[test]
459    fn test_build_controller_class_three_levels() {
460        let class = build_controller_class("operate", "admin.sub.Order");
461        assert_eq!(class, "addons\\operate\\controller\\admin\\sub\\Order");
462    }
463
464    #[test]
465    fn test_parse_route_valid() {
466        let (_tmp, registry, autoload) = make_test_env();
467
468        let route = parse_route("/addons/operate/Order/index", &registry, &autoload).unwrap();
469        assert_eq!(route.addon, "operate");
470        assert_eq!(route.controller, "Order");
471        assert_eq!(route.action, "index");
472        assert!(route.controller_file.is_some());
473    }
474
475    #[test]
476    fn test_parse_route_dotted_controller() {
477        let (_tmp, registry, autoload) = make_test_env();
478
479        let route = parse_route("/addons/operate/admin.Order/index", &registry, &autoload).unwrap();
480        assert_eq!(route.controller, "admin.Order");
481        assert_eq!(
482            route.controller_class,
483            "addons\\operate\\controller\\admin\\Order"
484        );
485        assert!(route.controller_file.is_some());
486        assert!(route
487            .controller_file
488            .unwrap()
489            .to_string_lossy()
490            .contains("admin"));
491    }
492
493    #[test]
494    fn test_parse_route_non_addons_url() {
495        let (_tmp, registry, autoload) = make_test_env();
496
497        let result = parse_route("/api/users", &registry, &autoload);
498        assert!(result.is_err());
499        match result.unwrap_err() {
500            AddonLoaderError::RouteParse { .. } => {}
501            other => panic!("expected RouteParse, got {:?}", other),
502        }
503    }
504
505    #[test]
506    fn test_parse_route_empty_action() {
507        let (_tmp, registry, autoload) = make_test_env();
508
509        let result = parse_route("/addons/operate/Order", &registry, &autoload);
510        assert!(result.is_err());
511        // action 为空应该触发 RouteParse 错误
512    }
513
514    #[test]
515    fn test_parse_route_empty_controller() {
516        let (_tmp, registry, autoload) = make_test_env();
517
518        let result = parse_route("/addons/operate", &registry, &autoload);
519        assert!(result.is_err());
520    }
521
522    #[test]
523    fn test_parse_route_addon_not_found() {
524        let (_tmp, registry, autoload) = make_test_env();
525
526        let result = parse_route("/addons/ghost/Order/index", &registry, &autoload);
527        assert!(result.is_err());
528        match result.unwrap_err() {
529            AddonLoaderError::AddonNotFound(name) => assert_eq!(name, "ghost"),
530            other => panic!("expected AddonNotFound, got {:?}", other),
531        }
532    }
533
534    #[test]
535    fn test_parse_route_addon_disabled() {
536        let (_tmp, registry, autoload) = make_test_env();
537
538        let result = parse_route("/addons/disabled/Order/index", &registry, &autoload);
539        assert!(result.is_err());
540        match result.unwrap_err() {
541            AddonLoaderError::AddonDisabled(name) => assert_eq!(name, "disabled"),
542            other => panic!("expected AddonDisabled, got {:?}", other),
543        }
544    }
545
546    #[test]
547    fn test_parse_route_controller_not_found() {
548        let (_tmp, registry, autoload) = make_test_env();
549
550        // nonexistent 插件无控制器
551        let result = parse_route("/addons/nonexistent/Ghost/index", &registry, &autoload);
552        assert!(result.is_err());
553        match result.unwrap_err() {
554            AddonLoaderError::ControllerNotFound(name) => assert_eq!(name, "Ghost"),
555            other => panic!("expected ControllerNotFound, got {:?}", other),
556        }
557    }
558
559    #[test]
560    fn test_parse_route_controller_file_resolved() {
561        let (_tmp, registry, autoload) = make_test_env();
562
563        let route = parse_route("/addons/operate/Order/index", &registry, &autoload).unwrap();
564        let file = route.controller_file.unwrap();
565        assert!(file.exists());
566        assert!(file.to_string_lossy().ends_with("Order.php"));
567    }
568
569    #[test]
570    fn test_parse_route_multilevel_controller_file_resolved() {
571        let (_tmp, registry, autoload) = make_test_env();
572
573        let route = parse_route("/addons/operate/admin.Order/index", &registry, &autoload).unwrap();
574        let file = route.controller_file.unwrap();
575        assert!(file.exists());
576        assert!(file.to_string_lossy().contains("admin"));
577        assert!(file.to_string_lossy().ends_with("Order.php"));
578    }
579
580    #[test]
581    fn test_parse_route_no_leading_slash() {
582        let (_tmp, registry, autoload) = make_test_env();
583
584        let route = parse_route("addons/operate/Order/index", &registry, &autoload).unwrap();
585        assert_eq!(route.addon, "operate");
586    }
587
588    #[test]
589    fn test_addon_route_clone_eq() {
590        let r1 = AddonRoute::new("a", "b", "c");
591        let r2 = r1.clone();
592        assert_eq!(r1, r2);
593    }
594
595    #[test]
596    fn test_addon_route_with_controller_file() {
597        let mut route = AddonRoute::new("a", "b", "c");
598        route.controller_file = Some(PathBuf::from("/addons/a/controller/B.php"));
599        assert_eq!(
600            route.controller_file(),
601            Some(std::path::Path::new("/addons/a/controller/B.php"))
602        );
603    }
604
605    // 直接测试 AddonManifest 的 status 字段对路由的影响
606    #[test]
607    fn test_route_status_check_reflects_manifest() {
608        let (_tmp, registry, autoload) = make_test_env();
609
610        // operate 启用
611        assert!(registry.is_enabled("operate").unwrap());
612
613        // disabled 禁用
614        assert!(!registry.is_enabled("disabled").unwrap());
615
616        // 动态启用 disabled
617        registry.set_enabled("disabled", true).unwrap();
618        assert!(registry.is_enabled("disabled").unwrap());
619
620        // disabled 启用后仍会因控制器不存在而失败(ControllerNotFound 而非 AddonDisabled)
621        let result = parse_route("/addons/disabled/Order/index", &registry, &autoload);
622        assert!(result.is_err());
623        match result.unwrap_err() {
624            AddonLoaderError::ControllerNotFound(_) => {}
625            AddonLoaderError::AddonDisabled(_) => {
626                panic!("should be ControllerNotFound after enabling")
627            }
628            other => panic!("unexpected error: {:?}", other),
629        }
630    }
631}