Skip to main content

sz_rust_core/
router.rs

1//! 路由模块 — 路由构建(基于 axum::Router)
2//!
3//! 对齐 PHP `with_route=true` + `auto_multi_app=true` + `app/controller/action` 路径解析。
4//!
5//! ## 功能
6//!
7//! - [`parse_path`]:将 URI 解析为 `(app, controller, action)` 三元组
8//! - [`ParsedPath`]:解析结果结构体
9//! - [`RouterBuilder`]:链式构建 axum::Router(支持 GET/POST/PUT/DELETE/资源路由)
10//!
11//! ## PHP 对齐
12//!
13//! 对齐 `config/app.php` + `config/route.php`:
14//!
15//! | PHP 配置项 | 值 | Rust 行为 |
16//! |-----------|----|----------|
17//! | `auto_multi_app` | `true` | 启用多应用解析 |
18//! | `app_map` | `oapc/admin/api/farm/oapi/cashier/scene` | 应用白名单 |
19//! | `default_app` | `index` | URI 无应用前缀时使用 |
20//! | `default_controller` | `Index` | URI 无控制器时使用 |
21//! | `default_action` | `index` | URI 无操作时使用 |
22//! | `deny_app_list` | `['common']` | 拒绝访问 `common` |
23//! | `controller_layer` | `controller` | 控制器层名 |
24//! | `pathinfo_depr` | `/` | 路径分隔符 |
25//! | `empty_controller` | `Error` | 空控制器名(暂未实现) |
26
27use axum::routing::{delete as route_delete, get, post, put};
28use axum::Router;
29use std::collections::HashSet;
30use std::sync::LazyLock;
31
32/// 已注册的应用白名单
33///
34/// 对齐 PHP `app_map`:`oapc / admin / api / farm / oapi / cashier / scene`
35pub static APP_MAP: LazyLock<HashSet<&'static str>> =
36    LazyLock::new(|| HashSet::from(["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"]));
37
38/// 禁止访问的应用列表
39///
40/// 对齐 PHP `deny_app_list = ['common']`
41pub static DENY_APP_LIST: LazyLock<HashSet<&'static str>> =
42    LazyLock::new(|| HashSet::from(["common"]));
43
44/// 默认应用名
45pub const DEFAULT_APP: &str = "index";
46/// 默认控制器名
47pub const DEFAULT_CONTROLLER: &str = "Index";
48/// 默认操作名
49pub const DEFAULT_ACTION: &str = "index";
50
51/// 路径解析结果
52///
53/// 对应 PHP 自动多应用解析出的 `(app, controller, action)` 三元组。
54///
55/// ## 字段说明
56///
57/// - `app`:应用名(如 `oapc` / `admin` / `index`)
58/// - `controller`:控制器名(PHP 习惯首字母大写,如 `Customer`)
59/// - `action`:操作名(小驼峰,如 `index` / `getList`)
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ParsedPath {
62    /// 应用名
63    pub app: String,
64    /// 控制器名(首字母大写)
65    pub controller: String,
66    /// 操作名(小驼峰)
67    pub action: String,
68}
69
70impl ParsedPath {
71    /// 构造函数(用于测试便捷)
72    pub fn new(
73        app: impl Into<String>,
74        controller: impl Into<String>,
75        action: impl Into<String>,
76    ) -> Self {
77        Self {
78            app: app.into(),
79            controller: controller.into(),
80            action: action.into(),
81        }
82    }
83}
84
85/// 解析 URI 路径为 `(app, controller, action)` 三元组
86///
87/// 对齐 PHP `auto_multi_app` 解析规则:
88///
89/// - `/` → `(index, Index, index)`
90/// - `/foo` → `(index, Foo, index)`
91/// - `/foo/bar` → `(index, Foo, bar)`
92/// - `/oapc/foo/bar` → `(oapc, Foo, bar)`(当 `oapc` 在 `app_map` 中)
93/// - `/common/foo/bar` → `(index, Common, foo)`(`common` 在 `deny_app_list` 中,当作控制器处理)
94///
95/// ## 参数
96///
97/// - `uri`:请求 URI(如 `/oapc/customer/index?id=1`),查询字符串会被自动剥离
98///
99/// ## 返回
100///
101/// 返回 [`ParsedPath`],永不为空。
102pub fn parse_path(uri: &str) -> ParsedPath {
103    // 剥离查询字符串
104    let path = uri.split('?').next().unwrap_or(uri);
105
106    // 剥离去前导 '/'
107    let path = path.trim_start_matches('/');
108
109    // 空路径 → 全部默认
110    if path.is_empty() {
111        return ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION);
112    }
113
114    // 按路径分隔符切分
115    let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
116
117    match segments.len() {
118        // /foo → (index, Foo, index)
119        1 => ParsedPath::new(DEFAULT_APP, capitalize_first(segments[0]), DEFAULT_ACTION),
120        // /foo/bar 或 /app/foo/bar
121        2 => {
122            // 第一段是 app_map 中的应用?
123            if is_app_in_map(segments[0]) {
124                ParsedPath::new(segments[0], capitalize_first(segments[1]), DEFAULT_ACTION)
125            } else {
126                // 不在 app_map,按 index/foo/bar 处理
127                ParsedPath::new(
128                    DEFAULT_APP,
129                    capitalize_first(segments[0]),
130                    segments[1].to_string(),
131                )
132            }
133        }
134        // /app/foo/bar 或 /foo/bar/baz 或更多
135        _ => {
136            if is_app_in_map(segments[0]) {
137                // (app, controller, action)
138                ParsedPath::new(
139                    segments[0],
140                    capitalize_first(segments[1]),
141                    segments[2].to_string(),
142                )
143            } else {
144                // (index, controller, action)
145                ParsedPath::new(
146                    DEFAULT_APP,
147                    capitalize_first(segments[0]),
148                    segments[1].to_string(),
149                )
150            }
151        }
152    }
153}
154
155/// 判断字符串是否在 `app_map` 中
156pub fn is_app_in_map(name: &str) -> bool {
157    APP_MAP.contains(name) && !DENY_APP_LIST.contains(name)
158}
159
160/// 首字母大写(对齐 PHP 控制器命名)
161///
162/// `customer` → `Customer`,`Customer` → `Customer`,`get_list` → `Get_list`(仅首字母大写)
163fn capitalize_first(s: &str) -> String {
164    let mut chars = s.chars();
165    match chars.next() {
166        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
167        None => String::new(),
168    }
169}
170
171/// 路由构建器
172///
173/// 对 axum::Router 的薄封装,提供链式 API 和未来扩展点(如 PHP 风格的
174/// `Route::group()` / `Route::resource()`)。
175///
176/// ## 用法
177///
178/// ```ignore
179/// use sz_rust_core::router::RouterBuilder;
180/// use axum::Json;
181///
182/// let router = RouterBuilder::new()
183///     .get("/ping", || async { "pong" })
184///     .post("/echo", |Json(v): Json<serde_json::Value>| async move { Json(v) })
185///     .build();
186/// ```
187/// 路由构建器(泛型状态支持)
188///
189/// `S` 为应用共享状态类型,由首次注册的路由 handler 推断。
190/// 当 handler 使用 `axum::extract::State<S>` 时,`S` 自动确定为该状态类型。
191///
192/// ## 用法
193///
194/// ```rust,ignore
195/// // 无状态路由
196/// let router = RouterBuilder::new()
197///     .get("/ping", ping_handler)
198///     .build();
199///
200/// // 有状态路由(S 由 handler 推断为 AppState)
201/// let router = RouterBuilder::new()
202///     .get("/items", list_items)  // list_items 使用 State<AppState>
203///     .with_state(AppState::default())
204///     .build();
205/// ```
206pub struct RouterBuilder<S = ()> {
207    inner: Router<S>,
208}
209
210impl<S> RouterBuilder<S>
211where
212    S: Clone + Send + Sync + 'static,
213{
214    /// 创建空的 RouterBuilder
215    pub fn new() -> Self {
216        Self {
217            inner: Router::new(),
218        }
219    }
220
221    /// 从已有 Router 起步
222    pub fn with_router(inner: Router<S>) -> Self {
223        Self { inner }
224    }
225
226    /// 注入应用状态并转换状态类型
227    ///
228    /// 当注册的 handler 使用了 `axum::extract::State<S>` 时调用,
229    /// 将 `RouterBuilder<S>` 转换为 `RouterBuilder<S2>`(通常 `S2 = S`)。
230    ///
231    /// ## 用法
232    ///
233    /// ```rust,ignore
234    /// let builder = RouterBuilder::new()
235    ///     .get("/items", list_items)
236    ///     .with_state(AppState { db: pool });
237    /// ```
238    pub fn with_state<S2>(self, state: S) -> RouterBuilder<S2> {
239        RouterBuilder {
240            inner: self.inner.with_state(state),
241        }
242    }
243
244    /// 注册 GET 路由
245    pub fn get<H, T>(self, path: &str, handler: H) -> Self
246    where
247        H: axum::handler::Handler<T, S>,
248        T: 'static,
249    {
250        Self {
251            inner: self.inner.route(path, get(handler)),
252        }
253    }
254
255    /// 注册 POST 路由
256    pub fn post<H, T>(self, path: &str, handler: H) -> Self
257    where
258        H: axum::handler::Handler<T, S>,
259        T: 'static,
260    {
261        Self {
262            inner: self.inner.route(path, post(handler)),
263        }
264    }
265
266    /// 注册 PUT 路由
267    pub fn put<H, T>(self, path: &str, handler: H) -> Self
268    where
269        H: axum::handler::Handler<T, S>,
270        T: 'static,
271    {
272        Self {
273            inner: self.inner.route(path, put(handler)),
274        }
275    }
276
277    /// 注册 DELETE 路由
278    pub fn delete<H, T>(self, path: &str, handler: H) -> Self
279    where
280        H: axum::handler::Handler<T, S>,
281        T: 'static,
282    {
283        Self {
284            inner: self.inner.route(path, route_delete(handler)),
285        }
286    }
287
288    /// 注册 WebSocket 路由(原生框架集成,无需独立端口)
289    ///
290    /// 在主 HTTP 端口上处理 WebSocket 升级请求(如 `GET /ws/chat`)。
291    /// 对齐 PHP Workerman `websocket://` 协议,但复用 HTTP 端口。
292    ///
293    /// ## 用法
294    ///
295    /// ```ignore
296    /// use sz_rust_core::router::RouterBuilder;
297    /// use sz_rust_core::websocket_route::EchoWsHandler;
298    ///
299    /// let router = RouterBuilder::new()
300    ///     .ws("/ws/echo", EchoWsHandler::new())
301    ///     .build();
302    /// ```
303    pub fn ws<H: crate::websocket_route::WsHandler>(self, path: &str, handler: H) -> Self {
304        let mr: axum::routing::MethodRouter<()> =
305            crate::websocket_route::ws_handler(handler);
306        let mr_s: axum::routing::MethodRouter<S> = mr.with_state(());
307        Self {
308            inner: self.inner.route(path, mr_s),
309        }
310    }
311
312    /// 应用一个 tower::Layer
313    ///
314    /// 约束与 `axum::Router::layer` 一致,便于直接链式调用任何 tower-http 中间件。
315    pub fn layer<L>(self, layer: L) -> Self
316    where
317        L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
318        L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
319        <L::Service as tower::Service<axum::extract::Request>>::Response:
320            axum::response::IntoResponse + 'static,
321        <L::Service as tower::Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
322        <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
323    {
324        Self {
325            inner: self.inner.layer(layer),
326        }
327    }
328
329    /// 合并另一个 Router
330    pub fn merge(self, other: Router<S>) -> Self {
331        Self {
332            inner: self.inner.merge(other),
333        }
334    }
335
336    /// 构建最终的 axum::Router
337    pub fn build(self) -> Router<S> {
338        self.inner
339    }
340}
341
342impl Default for RouterBuilder {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348// 仅供编译期验证 axum::Layer 类型约束使用
349use std::convert::Infallible;
350
351// ============================================================================
352// RESTful 资源路由(1.2.5)
353// ============================================================================
354
355/// RESTful 资源路由的 7 个标准 handler(每个可选)
356///
357/// 借鉴 Rails `resources :users` + Laravel `Route::resource`,一行注册
358/// 全部标准 RESTful 路由。
359///
360/// ## 标准路由映射
361///
362/// | Method   | Path                  | Handler   | 说明 |
363/// |----------|----------------------|-----------|------|
364/// | GET      | `/{name}`            | `index`   | 列表 |
365/// | GET      | `/{name}/create`     | `create`  | 新建表单(PHP 风格) |
366/// | POST     | `/{name}`            | `store`   | 保存 |
367/// | GET      | `/{name}/{id}`       | `show`    | 详情 |
368/// | GET      | `/{name}/{id}/edit`  | `edit`    | 编辑表单 |
369/// | PUT      | `/{name}/{id}`       | `update`  | 更新 |
370/// | DELETE   | `/{name}/{id}`       | `destroy` | 删除 |
371///
372/// ## 用法
373///
374/// ```ignore
375/// use sz_rust_core::router::{resource, ResourceRoutes};
376/// use axum::routing::{get, post, put, delete};
377///
378/// let routes = ResourceRoutes {
379///     index: Some(get(|| async { "list" })),
380///     show: Some(get(|| async { "show" })),
381///     store: Some(post(|| async { "create" })),
382///     update: Some(put(|| async { "update" })),
383///     destroy: Some(delete(|| async { "delete" })),
384///     ..Default::default()
385/// };
386/// let router = resource("users", routes);
387/// ```
388#[derive(Default)]
389pub struct ResourceRoutes {
390    /// GET `/{name}` — 列表
391    pub index: Option<axum::routing::MethodRouter>,
392    /// GET `/{name}/create` — 新建表单
393    pub create: Option<axum::routing::MethodRouter>,
394    /// POST `/{name}` — 保存
395    pub store: Option<axum::routing::MethodRouter>,
396    /// GET `/{name}/{id}` — 详情
397    pub show: Option<axum::routing::MethodRouter>,
398    /// GET `/{name}/{id}/edit` — 编辑表单
399    pub edit: Option<axum::routing::MethodRouter>,
400    /// PUT `/{name}/{id}` — 更新
401    pub update: Option<axum::routing::MethodRouter>,
402    /// DELETE `/{name}/{id}` — 删除
403    pub destroy: Option<axum::routing::MethodRouter>,
404}
405
406impl ResourceRoutes {
407    /// 创建空 routes(所有 handler 都为 None)
408    pub fn new() -> Self {
409        Self::default()
410    }
411}
412
413/// 构造 RESTful 资源路由
414///
415/// 根据 `ResourceRoutes` 中提供的非 None handler,注册对应的标准路由。
416/// 缺省的 handler(None)将不会被注册(请求返回 404)。
417///
418/// ## 参数
419///
420/// - `name`:资源名称(如 `"users"`),用于构造路径 `/{name}` 和 `/{name}/{id}`
421/// - `routes`:7 个可选 handler
422///
423/// ## 返回
424///
425/// 一个独立的 `axum::Router`,可与其他 Router `merge()` 合并。
426///
427/// ## 路径模板
428///
429/// - `/{name}` → GET index + POST store(合并到同一 MethodRouter)
430/// - `/{name}/create` → GET create
431/// - `/{name}/{id}` → GET show + PUT update + DELETE destroy
432/// - `/{name}/{id}/edit` → GET edit
433///
434/// ## 用法
435///
436/// ```ignore
437/// use sz_rust_core::router::{resource, ResourceRoutes};
438///
439/// let routes = ResourceRoutes {
440///     index: Some(axum::routing::get(|| async { "list" })),
441///     store: Some(axum::routing::post(|| async { "create" })),
442///     ..Default::default()
443/// };
444/// let router = resource("users", routes);
445/// ```
446pub fn resource(name: &str, routes: ResourceRoutes) -> axum::Router {
447    let base = format!("/{name}");
448    let with_id = format!("/{name}/{{id}}");
449    let create_path = format!("/{name}/create");
450    let edit_path = format!("/{name}/{{id}}/edit");
451
452    let mut router = axum::Router::new();
453
454    // /{name}: GET index + POST store
455    let mut base_methods = axum::routing::MethodRouter::new();
456    let mut has_base = false;
457    if let Some(h) = routes.index {
458        base_methods = base_methods.merge(h);
459        has_base = true;
460    }
461    if let Some(h) = routes.store {
462        base_methods = base_methods.merge(h);
463        has_base = true;
464    }
465    if has_base {
466        router = router.route(&base, base_methods);
467    }
468
469    // /{name}/create: GET create
470    if let Some(h) = routes.create {
471        router = router.route(&create_path, h);
472    }
473
474    // /{name}/{id}: GET show + PUT update + DELETE destroy
475    let mut id_methods = axum::routing::MethodRouter::new();
476    let mut has_id = false;
477    if let Some(h) = routes.show {
478        id_methods = id_methods.merge(h);
479        has_id = true;
480    }
481    if let Some(h) = routes.update {
482        id_methods = id_methods.merge(h);
483        has_id = true;
484    }
485    if let Some(h) = routes.destroy {
486        id_methods = id_methods.merge(h);
487        has_id = true;
488    }
489    if has_id {
490        router = router.route(&with_id, id_methods);
491    }
492
493    // /{name}/{id}/edit: GET edit
494    if let Some(h) = routes.edit {
495        router = router.route(&edit_path, h);
496    }
497
498    router
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504    use axum::body::Body;
505    use axum::http::{Method, Request, StatusCode};
506    use http_body_util::BodyExt;
507    use tower::ServiceExt;
508
509    // ====================================================================
510    // parse_path 单元测试
511    // ====================================================================
512
513    #[test]
514    fn test_parse_path_root() {
515        let p = parse_path("/");
516        assert_eq!(p, ParsedPath::new("index", "Index", "index"));
517    }
518
519    #[test]
520    fn test_parse_path_empty() {
521        let p = parse_path("");
522        assert_eq!(p, ParsedPath::new("index", "Index", "index"));
523    }
524
525    #[test]
526    fn test_parse_path_single_segment() {
527        let p = parse_path("/customer");
528        assert_eq!(p, ParsedPath::new("index", "Customer", "index"));
529    }
530
531    #[test]
532    fn test_parse_path_two_segments_no_app() {
533        let p = parse_path("/customer/list");
534        assert_eq!(p, ParsedPath::new("index", "Customer", "list"));
535    }
536
537    #[test]
538    fn test_parse_path_three_segments_with_app() {
539        let p = parse_path("/oapc/customer/index");
540        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
541    }
542
543    #[test]
544    fn test_parse_path_app_in_map_two_segments() {
545        let p = parse_path("/admin/login");
546        assert_eq!(p, ParsedPath::new("admin", "Login", "index"));
547    }
548
549    #[test]
550    fn test_parse_path_all_seven_apps() {
551        for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
552            let p = parse_path(&format!("/{app}/customer/index"));
553            assert_eq!(p, ParsedPath::new(app, "Customer", "index"));
554        }
555    }
556
557    #[test]
558    fn test_parse_path_deny_common_app() {
559        // common 在 deny_app_list 中,应当被当作控制器处理
560        let p = parse_path("/common/customer/index");
561        assert_eq!(p, ParsedPath::new("index", "Common", "customer"));
562    }
563
564    #[test]
565    fn test_parse_path_with_query_string() {
566        let p = parse_path("/oapc/customer/index?id=1&page=2");
567        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
568    }
569
570    #[test]
571    fn test_parse_path_with_trailing_slash() {
572        let p = parse_path("/oapc/customer/index/");
573        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
574    }
575
576    #[test]
577    fn test_parse_path_double_slash() {
578        let p = parse_path("//oapc//customer//index");
579        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
580    }
581
582    #[test]
583    fn test_parse_path_capitalize_first_only() {
584        // 仅首字母大写,不强制后续小写
585        let p = parse_path("/customerList");
586        assert_eq!(p, ParsedPath::new("index", "CustomerList", "index"));
587    }
588
589    #[test]
590    fn test_is_app_in_map_all_seven() {
591        for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
592            assert!(is_app_in_map(app), "{app} should be in app_map");
593        }
594    }
595
596    #[test]
597    fn test_is_app_in_map_deny_common() {
598        assert!(!is_app_in_map("common"));
599    }
600
601    #[test]
602    fn test_is_app_in_map_unknown_app() {
603        assert!(!is_app_in_map("unknown"));
604        assert!(!is_app_in_map(""));
605    }
606
607    // ====================================================================
608    // RouterBuilder 单元测试
609    // ====================================================================
610
611    #[tokio::test]
612    async fn test_router_builder_get() {
613        let router = RouterBuilder::new()
614            .get("/ping", || async { "pong" })
615            .build();
616        let request = Request::builder()
617            .method(Method::GET)
618            .uri("/ping")
619            .body(Body::empty())
620            .unwrap();
621        let response = router.oneshot(request).await.unwrap();
622        assert_eq!(response.status(), StatusCode::OK);
623        let bytes = response.into_body().collect().await.unwrap().to_bytes();
624        assert_eq!(&bytes[..], b"pong");
625    }
626
627    #[tokio::test]
628    async fn test_router_builder_post() {
629        let router = RouterBuilder::new()
630            .post("/echo", |body: Body| async move {
631                let bytes = body
632                    .collect()
633                    .await
634                    .map_err(|_| ())
635                    .map(|b| b.to_bytes())
636                    .unwrap_or_default();
637                String::from_utf8_lossy(&bytes).to_string()
638            })
639            .build();
640        let request = Request::builder()
641            .method(Method::POST)
642            .uri("/echo")
643            .body(Body::from("hello"))
644            .unwrap();
645        let response = router.oneshot(request).await.unwrap();
646        assert_eq!(response.status(), StatusCode::OK);
647        let bytes = response.into_body().collect().await.unwrap().to_bytes();
648        assert_eq!(&bytes[..], b"hello");
649    }
650
651    #[tokio::test]
652    async fn test_router_builder_multiple_methods() {
653        let router = RouterBuilder::new()
654            .get("/items", || async { "list" })
655            .post("/items", || async { "create" })
656            .put("/items/1", || async { "update" })
657            .delete("/items/1", || async { "delete" })
658            .build();
659
660        // GET
661        let req = Request::builder()
662            .method(Method::GET)
663            .uri("/items")
664            .body(Body::empty())
665            .unwrap();
666        let resp = router.clone().oneshot(req).await.unwrap();
667        assert_eq!(resp.status(), StatusCode::OK);
668
669        // POST
670        let req = Request::builder()
671            .method(Method::POST)
672            .uri("/items")
673            .body(Body::empty())
674            .unwrap();
675        let resp = router.clone().oneshot(req).await.unwrap();
676        assert_eq!(resp.status(), StatusCode::OK);
677
678        // PUT
679        let req = Request::builder()
680            .method(Method::PUT)
681            .uri("/items/1")
682            .body(Body::empty())
683            .unwrap();
684        let resp = router.clone().oneshot(req).await.unwrap();
685        assert_eq!(resp.status(), StatusCode::OK);
686
687        // DELETE
688        let req = Request::builder()
689            .method(Method::DELETE)
690            .uri("/items/1")
691            .body(Body::empty())
692            .unwrap();
693        let resp = router.oneshot(req).await.unwrap();
694        assert_eq!(resp.status(), StatusCode::OK);
695    }
696
697    #[tokio::test]
698    async fn test_router_builder_not_found() {
699        let router = RouterBuilder::new()
700            .get("/ping", || async { "pong" })
701            .build();
702        let request = Request::builder()
703            .method(Method::GET)
704            .uri("/unknown")
705            .body(Body::empty())
706            .unwrap();
707        let response = router.oneshot(request).await.unwrap();
708        assert_eq!(response.status(), StatusCode::NOT_FOUND);
709    }
710
711    #[tokio::test]
712    async fn test_router_builder_merge() {
713        let r1 = RouterBuilder::new().get("/a", || async { "A" }).build();
714        let r2 = RouterBuilder::new().get("/b", || async { "B" }).build();
715        let router = RouterBuilder::new().merge(r1).merge(r2).build();
716
717        for path in ["/a", "/b"] {
718            let request = Request::builder()
719                .method(Method::GET)
720                .uri(path)
721                .body(Body::empty())
722                .unwrap();
723            let response = router.clone().oneshot(request).await.unwrap();
724            assert_eq!(response.status(), StatusCode::OK);
725        }
726    }
727
728    #[tokio::test]
729    async fn test_router_builder_default() {
730        let router = RouterBuilder::default().build();
731        let request = Request::builder()
732            .method(Method::GET)
733            .uri("/")
734            .body(Body::empty())
735            .unwrap();
736        let response = router.oneshot(request).await.unwrap();
737        assert_eq!(response.status(), StatusCode::NOT_FOUND);
738    }
739
740    // ====================================================================
741    // resource() RESTful 资源路由测试(1.2.5)
742    // ====================================================================
743
744    #[tokio::test]
745    async fn test_resource_all_seven_handlers() {
746        let routes = ResourceRoutes {
747            index: Some(axum::routing::get(|| async { "index" })),
748            create: Some(axum::routing::get(|| async { "create" })),
749            store: Some(axum::routing::post(|| async { "store" })),
750            show: Some(axum::routing::get(|| async { "show" })),
751            edit: Some(axum::routing::get(|| async { "edit" })),
752            update: Some(axum::routing::put(|| async { "update" })),
753            destroy: Some(axum::routing::delete(|| async { "destroy" })),
754        };
755        let router = resource("users", routes);
756
757        // GET /users → index
758        let req = Request::builder()
759            .method(Method::GET)
760            .uri("/users")
761            .body(Body::empty())
762            .unwrap();
763        let resp = router.clone().oneshot(req).await.unwrap();
764        assert_eq!(resp.status(), StatusCode::OK);
765
766        // POST /users → store
767        let req = Request::builder()
768            .method(Method::POST)
769            .uri("/users")
770            .body(Body::empty())
771            .unwrap();
772        let resp = router.clone().oneshot(req).await.unwrap();
773        assert_eq!(resp.status(), StatusCode::OK);
774
775        // GET /users/create → create
776        let req = Request::builder()
777            .method(Method::GET)
778            .uri("/users/create")
779            .body(Body::empty())
780            .unwrap();
781        let resp = router.clone().oneshot(req).await.unwrap();
782        assert_eq!(resp.status(), StatusCode::OK);
783
784        // GET /users/1 → show
785        let req = Request::builder()
786            .method(Method::GET)
787            .uri("/users/1")
788            .body(Body::empty())
789            .unwrap();
790        let resp = router.clone().oneshot(req).await.unwrap();
791        assert_eq!(resp.status(), StatusCode::OK);
792
793        // GET /users/1/edit → edit
794        let req = Request::builder()
795            .method(Method::GET)
796            .uri("/users/1/edit")
797            .body(Body::empty())
798            .unwrap();
799        let resp = router.clone().oneshot(req).await.unwrap();
800        assert_eq!(resp.status(), StatusCode::OK);
801
802        // PUT /users/1 → update
803        let req = Request::builder()
804            .method(Method::PUT)
805            .uri("/users/1")
806            .body(Body::empty())
807            .unwrap();
808        let resp = router.clone().oneshot(req).await.unwrap();
809        assert_eq!(resp.status(), StatusCode::OK);
810
811        // DELETE /users/1 → destroy
812        let req = Request::builder()
813            .method(Method::DELETE)
814            .uri("/users/1")
815            .body(Body::empty())
816            .unwrap();
817        let resp = router.oneshot(req).await.unwrap();
818        assert_eq!(resp.status(), StatusCode::OK);
819    }
820
821    #[tokio::test]
822    async fn test_resource_partial_handlers_only_index_and_store() {
823        let routes = ResourceRoutes {
824            index: Some(axum::routing::get(|| async { "list" })),
825            store: Some(axum::routing::post(|| async { "create" })),
826            ..Default::default()
827        };
828        let router = resource("articles", routes);
829
830        // GET /articles → 200
831        let req = Request::builder()
832            .method(Method::GET)
833            .uri("/articles")
834            .body(Body::empty())
835            .unwrap();
836        let resp = router.clone().oneshot(req).await.unwrap();
837        assert_eq!(resp.status(), StatusCode::OK);
838
839        // POST /articles → 200
840        let req = Request::builder()
841            .method(Method::POST)
842            .uri("/articles")
843            .body(Body::empty())
844            .unwrap();
845        let resp = router.clone().oneshot(req).await.unwrap();
846        assert_eq!(resp.status(), StatusCode::OK);
847
848        // GET /articles/1 → 404(show 未注册)
849        let req = Request::builder()
850            .method(Method::GET)
851            .uri("/articles/1")
852            .body(Body::empty())
853            .unwrap();
854        let resp = router.clone().oneshot(req).await.unwrap();
855        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
856
857        // GET /articles/create → 404
858        let req = Request::builder()
859            .method(Method::GET)
860            .uri("/articles/create")
861            .body(Body::empty())
862            .unwrap();
863        let resp = router.oneshot(req).await.unwrap();
864        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
865    }
866
867    #[tokio::test]
868    async fn test_resource_only_id_routes() {
869        let routes = ResourceRoutes {
870            show: Some(axum::routing::get(|| async { "show" })),
871            update: Some(axum::routing::put(|| async { "update" })),
872            destroy: Some(axum::routing::delete(|| async { "destroy" })),
873            ..Default::default()
874        };
875        let router = resource("orders", routes);
876
877        // GET /orders → 404(index/store 未注册)
878        let req = Request::builder()
879            .method(Method::GET)
880            .uri("/orders")
881            .body(Body::empty())
882            .unwrap();
883        let resp = router.clone().oneshot(req).await.unwrap();
884        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
885
886        // GET /orders/1 → 200
887        let req = Request::builder()
888            .method(Method::GET)
889            .uri("/orders/1")
890            .body(Body::empty())
891            .unwrap();
892        let resp = router.clone().oneshot(req).await.unwrap();
893        assert_eq!(resp.status(), StatusCode::OK);
894
895        // PUT /orders/1 → 200
896        let req = Request::builder()
897            .method(Method::PUT)
898            .uri("/orders/1")
899            .body(Body::empty())
900            .unwrap();
901        let resp = router.clone().oneshot(req).await.unwrap();
902        assert_eq!(resp.status(), StatusCode::OK);
903
904        // DELETE /orders/1 → 200
905        let req = Request::builder()
906            .method(Method::DELETE)
907            .uri("/orders/1")
908            .body(Body::empty())
909            .unwrap();
910        let resp = router.oneshot(req).await.unwrap();
911        assert_eq!(resp.status(), StatusCode::OK);
912    }
913
914    #[tokio::test]
915    async fn test_resource_empty_routes() {
916        // 全部 None → 不注册任何路由 → 所有请求 404
917        let routes = ResourceRoutes::new();
918        let router = resource("widgets", routes);
919
920        let req = Request::builder()
921            .method(Method::GET)
922            .uri("/widgets")
923            .body(Body::empty())
924            .unwrap();
925        let resp = router.oneshot(req).await.unwrap();
926        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
927    }
928
929    #[tokio::test]
930    async fn test_resource_merged_into_router_builder() {
931        let routes = ResourceRoutes {
932            index: Some(axum::routing::get(|| async { "list" })),
933            store: Some(axum::routing::post(|| async { "create" })),
934            show: Some(axum::routing::get(|| async { "show" })),
935            ..Default::default()
936        };
937        let resource_router = resource("users", routes);
938
939        let router = RouterBuilder::new()
940            .merge(resource_router)
941            .get("/health", || async { "ok" })
942            .build();
943
944        // GET /health
945        let req = Request::builder()
946            .method(Method::GET)
947            .uri("/health")
948            .body(Body::empty())
949            .unwrap();
950        let resp = router.clone().oneshot(req).await.unwrap();
951        assert_eq!(resp.status(), StatusCode::OK);
952
953        // GET /users
954        let req = Request::builder()
955            .method(Method::GET)
956            .uri("/users")
957            .body(Body::empty())
958            .unwrap();
959        let resp = router.clone().oneshot(req).await.unwrap();
960        assert_eq!(resp.status(), StatusCode::OK);
961
962        // POST /users
963        let req = Request::builder()
964            .method(Method::POST)
965            .uri("/users")
966            .body(Body::empty())
967            .unwrap();
968        let resp = router.clone().oneshot(req).await.unwrap();
969        assert_eq!(resp.status(), StatusCode::OK);
970
971        // GET /users/42
972        let req = Request::builder()
973            .method(Method::GET)
974            .uri("/users/42")
975            .body(Body::empty())
976            .unwrap();
977        let resp = router.oneshot(req).await.unwrap();
978        assert_eq!(resp.status(), StatusCode::OK);
979    }
980
981    #[tokio::test]
982    async fn test_resource_body_content() {
983        let routes = ResourceRoutes {
984            index: Some(axum::routing::get(|| async { "user list" })),
985            ..Default::default()
986        };
987        let router = resource("users", routes);
988
989        let req = Request::builder()
990            .method(Method::GET)
991            .uri("/users")
992            .body(Body::empty())
993            .unwrap();
994        let resp = router.oneshot(req).await.unwrap();
995        assert_eq!(resp.status(), StatusCode::OK);
996        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
997        assert_eq!(&bytes[..], b"user list");
998    }
999}