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/// ```
187pub struct RouterBuilder {
188    inner: Router,
189}
190
191impl RouterBuilder {
192    /// 创建空的 RouterBuilder
193    pub fn new() -> Self {
194        Self {
195            inner: Router::new(),
196        }
197    }
198
199    /// 从已有 Router 起步
200    pub fn with_router(router: Router) -> Self {
201        Self { inner: router }
202    }
203
204    /// 注册 GET 路由
205    pub fn get<H, T>(self, path: &str, handler: H) -> Self
206    where
207        H: axum::handler::Handler<T, ()>,
208        T: 'static,
209    {
210        Self {
211            inner: self.inner.route(path, get(handler)),
212        }
213    }
214
215    /// 注册 POST 路由
216    pub fn post<H, T>(self, path: &str, handler: H) -> Self
217    where
218        H: axum::handler::Handler<T, ()>,
219        T: 'static,
220    {
221        Self {
222            inner: self.inner.route(path, post(handler)),
223        }
224    }
225
226    /// 注册 PUT 路由
227    pub fn put<H, T>(self, path: &str, handler: H) -> Self
228    where
229        H: axum::handler::Handler<T, ()>,
230        T: 'static,
231    {
232        Self {
233            inner: self.inner.route(path, put(handler)),
234        }
235    }
236
237    /// 注册 DELETE 路由
238    pub fn delete<H, T>(self, path: &str, handler: H) -> Self
239    where
240        H: axum::handler::Handler<T, ()>,
241        T: 'static,
242    {
243        Self {
244            inner: self.inner.route(path, route_delete(handler)),
245        }
246    }
247
248    /// 应用一个 tower::Layer
249    ///
250    /// 约束与 `axum::Router::layer` 一致,便于直接链式调用任何 tower-http 中间件。
251    pub fn layer<L>(self, layer: L) -> Self
252    where
253        L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
254        L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
255        <L::Service as tower::Service<axum::extract::Request>>::Response:
256            axum::response::IntoResponse + 'static,
257        <L::Service as tower::Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
258        <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
259    {
260        Self {
261            inner: self.inner.layer(layer),
262        }
263    }
264
265    /// 合并另一个 Router
266    pub fn merge(self, other: Router) -> Self {
267        Self {
268            inner: self.inner.merge(other),
269        }
270    }
271
272    /// 构建最终的 axum::Router
273    pub fn build(self) -> Router {
274        self.inner
275    }
276}
277
278impl Default for RouterBuilder {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284// 仅供编译期验证 axum::Layer 类型约束使用
285use std::convert::Infallible;
286
287// ============================================================================
288// RESTful 资源路由(1.2.5)
289// ============================================================================
290
291/// RESTful 资源路由的 7 个标准 handler(每个可选)
292///
293/// 借鉴 Rails `resources :users` + Laravel `Route::resource`,一行注册
294/// 全部标准 RESTful 路由。
295///
296/// ## 标准路由映射
297///
298/// | Method   | Path                  | Handler   | 说明 |
299/// |----------|----------------------|-----------|------|
300/// | GET      | `/{name}`            | `index`   | 列表 |
301/// | GET      | `/{name}/create`     | `create`  | 新建表单(PHP 风格) |
302/// | POST     | `/{name}`            | `store`   | 保存 |
303/// | GET      | `/{name}/{id}`       | `show`    | 详情 |
304/// | GET      | `/{name}/{id}/edit`  | `edit`    | 编辑表单 |
305/// | PUT      | `/{name}/{id}`       | `update`  | 更新 |
306/// | DELETE   | `/{name}/{id}`       | `destroy` | 删除 |
307///
308/// ## 用法
309///
310/// ```ignore
311/// use sz_rust_core::router::{resource, ResourceRoutes};
312/// use axum::routing::{get, post, put, delete};
313///
314/// let routes = ResourceRoutes {
315///     index: Some(get(|| async { "list" })),
316///     show: Some(get(|| async { "show" })),
317///     store: Some(post(|| async { "create" })),
318///     update: Some(put(|| async { "update" })),
319///     destroy: Some(delete(|| async { "delete" })),
320///     ..Default::default()
321/// };
322/// let router = resource("users", routes);
323/// ```
324#[derive(Default)]
325pub struct ResourceRoutes {
326    /// GET `/{name}` — 列表
327    pub index: Option<axum::routing::MethodRouter>,
328    /// GET `/{name}/create` — 新建表单
329    pub create: Option<axum::routing::MethodRouter>,
330    /// POST `/{name}` — 保存
331    pub store: Option<axum::routing::MethodRouter>,
332    /// GET `/{name}/{id}` — 详情
333    pub show: Option<axum::routing::MethodRouter>,
334    /// GET `/{name}/{id}/edit` — 编辑表单
335    pub edit: Option<axum::routing::MethodRouter>,
336    /// PUT `/{name}/{id}` — 更新
337    pub update: Option<axum::routing::MethodRouter>,
338    /// DELETE `/{name}/{id}` — 删除
339    pub destroy: Option<axum::routing::MethodRouter>,
340}
341
342impl ResourceRoutes {
343    /// 创建空 routes(所有 handler 都为 None)
344    pub fn new() -> Self {
345        Self::default()
346    }
347}
348
349/// 构造 RESTful 资源路由
350///
351/// 根据 `ResourceRoutes` 中提供的非 None handler,注册对应的标准路由。
352/// 缺省的 handler(None)将不会被注册(请求返回 404)。
353///
354/// ## 参数
355///
356/// - `name`:资源名称(如 `"users"`),用于构造路径 `/{name}` 和 `/{name}/{id}`
357/// - `routes`:7 个可选 handler
358///
359/// ## 返回
360///
361/// 一个独立的 `axum::Router`,可与其他 Router `merge()` 合并。
362///
363/// ## 路径模板
364///
365/// - `/{name}` → GET index + POST store(合并到同一 MethodRouter)
366/// - `/{name}/create` → GET create
367/// - `/{name}/{id}` → GET show + PUT update + DELETE destroy
368/// - `/{name}/{id}/edit` → GET edit
369///
370/// ## 用法
371///
372/// ```ignore
373/// use sz_rust_core::router::{resource, ResourceRoutes};
374///
375/// let routes = ResourceRoutes {
376///     index: Some(axum::routing::get(|| async { "list" })),
377///     store: Some(axum::routing::post(|| async { "create" })),
378///     ..Default::default()
379/// };
380/// let router = resource("users", routes);
381/// ```
382pub fn resource(name: &str, routes: ResourceRoutes) -> axum::Router {
383    let base = format!("/{name}");
384    let with_id = format!("/{name}/{{id}}");
385    let create_path = format!("/{name}/create");
386    let edit_path = format!("/{name}/{{id}}/edit");
387
388    let mut router = axum::Router::new();
389
390    // /{name}: GET index + POST store
391    let mut base_methods = axum::routing::MethodRouter::new();
392    let mut has_base = false;
393    if let Some(h) = routes.index {
394        base_methods = base_methods.merge(h);
395        has_base = true;
396    }
397    if let Some(h) = routes.store {
398        base_methods = base_methods.merge(h);
399        has_base = true;
400    }
401    if has_base {
402        router = router.route(&base, base_methods);
403    }
404
405    // /{name}/create: GET create
406    if let Some(h) = routes.create {
407        router = router.route(&create_path, h);
408    }
409
410    // /{name}/{id}: GET show + PUT update + DELETE destroy
411    let mut id_methods = axum::routing::MethodRouter::new();
412    let mut has_id = false;
413    if let Some(h) = routes.show {
414        id_methods = id_methods.merge(h);
415        has_id = true;
416    }
417    if let Some(h) = routes.update {
418        id_methods = id_methods.merge(h);
419        has_id = true;
420    }
421    if let Some(h) = routes.destroy {
422        id_methods = id_methods.merge(h);
423        has_id = true;
424    }
425    if has_id {
426        router = router.route(&with_id, id_methods);
427    }
428
429    // /{name}/{id}/edit: GET edit
430    if let Some(h) = routes.edit {
431        router = router.route(&edit_path, h);
432    }
433
434    router
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440    use axum::body::Body;
441    use axum::http::{Method, Request, StatusCode};
442    use http_body_util::BodyExt;
443    use tower::ServiceExt;
444
445    // ====================================================================
446    // parse_path 单元测试
447    // ====================================================================
448
449    #[test]
450    fn test_parse_path_root() {
451        let p = parse_path("/");
452        assert_eq!(p, ParsedPath::new("index", "Index", "index"));
453    }
454
455    #[test]
456    fn test_parse_path_empty() {
457        let p = parse_path("");
458        assert_eq!(p, ParsedPath::new("index", "Index", "index"));
459    }
460
461    #[test]
462    fn test_parse_path_single_segment() {
463        let p = parse_path("/customer");
464        assert_eq!(p, ParsedPath::new("index", "Customer", "index"));
465    }
466
467    #[test]
468    fn test_parse_path_two_segments_no_app() {
469        let p = parse_path("/customer/list");
470        assert_eq!(p, ParsedPath::new("index", "Customer", "list"));
471    }
472
473    #[test]
474    fn test_parse_path_three_segments_with_app() {
475        let p = parse_path("/oapc/customer/index");
476        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
477    }
478
479    #[test]
480    fn test_parse_path_app_in_map_two_segments() {
481        let p = parse_path("/admin/login");
482        assert_eq!(p, ParsedPath::new("admin", "Login", "index"));
483    }
484
485    #[test]
486    fn test_parse_path_all_seven_apps() {
487        for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
488            let p = parse_path(&format!("/{app}/customer/index"));
489            assert_eq!(p, ParsedPath::new(app, "Customer", "index"));
490        }
491    }
492
493    #[test]
494    fn test_parse_path_deny_common_app() {
495        // common 在 deny_app_list 中,应当被当作控制器处理
496        let p = parse_path("/common/customer/index");
497        assert_eq!(p, ParsedPath::new("index", "Common", "customer"));
498    }
499
500    #[test]
501    fn test_parse_path_with_query_string() {
502        let p = parse_path("/oapc/customer/index?id=1&page=2");
503        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
504    }
505
506    #[test]
507    fn test_parse_path_with_trailing_slash() {
508        let p = parse_path("/oapc/customer/index/");
509        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
510    }
511
512    #[test]
513    fn test_parse_path_double_slash() {
514        let p = parse_path("//oapc//customer//index");
515        assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
516    }
517
518    #[test]
519    fn test_parse_path_capitalize_first_only() {
520        // 仅首字母大写,不强制后续小写
521        let p = parse_path("/customerList");
522        assert_eq!(p, ParsedPath::new("index", "CustomerList", "index"));
523    }
524
525    #[test]
526    fn test_is_app_in_map_all_seven() {
527        for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
528            assert!(is_app_in_map(app), "{app} should be in app_map");
529        }
530    }
531
532    #[test]
533    fn test_is_app_in_map_deny_common() {
534        assert!(!is_app_in_map("common"));
535    }
536
537    #[test]
538    fn test_is_app_in_map_unknown_app() {
539        assert!(!is_app_in_map("unknown"));
540        assert!(!is_app_in_map(""));
541    }
542
543    // ====================================================================
544    // RouterBuilder 单元测试
545    // ====================================================================
546
547    #[tokio::test]
548    async fn test_router_builder_get() {
549        let router = RouterBuilder::new()
550            .get("/ping", || async { "pong" })
551            .build();
552        let request = Request::builder()
553            .method(Method::GET)
554            .uri("/ping")
555            .body(Body::empty())
556            .unwrap();
557        let response = router.oneshot(request).await.unwrap();
558        assert_eq!(response.status(), StatusCode::OK);
559        let bytes = response.into_body().collect().await.unwrap().to_bytes();
560        assert_eq!(&bytes[..], b"pong");
561    }
562
563    #[tokio::test]
564    async fn test_router_builder_post() {
565        let router = RouterBuilder::new()
566            .post("/echo", |body: Body| async move {
567                let bytes = body
568                    .collect()
569                    .await
570                    .map_err(|_| ())
571                    .map(|b| b.to_bytes())
572                    .unwrap_or_default();
573                String::from_utf8_lossy(&bytes).to_string()
574            })
575            .build();
576        let request = Request::builder()
577            .method(Method::POST)
578            .uri("/echo")
579            .body(Body::from("hello"))
580            .unwrap();
581        let response = router.oneshot(request).await.unwrap();
582        assert_eq!(response.status(), StatusCode::OK);
583        let bytes = response.into_body().collect().await.unwrap().to_bytes();
584        assert_eq!(&bytes[..], b"hello");
585    }
586
587    #[tokio::test]
588    async fn test_router_builder_multiple_methods() {
589        let router = RouterBuilder::new()
590            .get("/items", || async { "list" })
591            .post("/items", || async { "create" })
592            .put("/items/1", || async { "update" })
593            .delete("/items/1", || async { "delete" })
594            .build();
595
596        // GET
597        let req = Request::builder()
598            .method(Method::GET)
599            .uri("/items")
600            .body(Body::empty())
601            .unwrap();
602        let resp = router.clone().oneshot(req).await.unwrap();
603        assert_eq!(resp.status(), StatusCode::OK);
604
605        // POST
606        let req = Request::builder()
607            .method(Method::POST)
608            .uri("/items")
609            .body(Body::empty())
610            .unwrap();
611        let resp = router.clone().oneshot(req).await.unwrap();
612        assert_eq!(resp.status(), StatusCode::OK);
613
614        // PUT
615        let req = Request::builder()
616            .method(Method::PUT)
617            .uri("/items/1")
618            .body(Body::empty())
619            .unwrap();
620        let resp = router.clone().oneshot(req).await.unwrap();
621        assert_eq!(resp.status(), StatusCode::OK);
622
623        // DELETE
624        let req = Request::builder()
625            .method(Method::DELETE)
626            .uri("/items/1")
627            .body(Body::empty())
628            .unwrap();
629        let resp = router.oneshot(req).await.unwrap();
630        assert_eq!(resp.status(), StatusCode::OK);
631    }
632
633    #[tokio::test]
634    async fn test_router_builder_not_found() {
635        let router = RouterBuilder::new()
636            .get("/ping", || async { "pong" })
637            .build();
638        let request = Request::builder()
639            .method(Method::GET)
640            .uri("/unknown")
641            .body(Body::empty())
642            .unwrap();
643        let response = router.oneshot(request).await.unwrap();
644        assert_eq!(response.status(), StatusCode::NOT_FOUND);
645    }
646
647    #[tokio::test]
648    async fn test_router_builder_merge() {
649        let r1 = RouterBuilder::new().get("/a", || async { "A" }).build();
650        let r2 = RouterBuilder::new().get("/b", || async { "B" }).build();
651        let router = RouterBuilder::new().merge(r1).merge(r2).build();
652
653        for path in ["/a", "/b"] {
654            let request = Request::builder()
655                .method(Method::GET)
656                .uri(path)
657                .body(Body::empty())
658                .unwrap();
659            let response = router.clone().oneshot(request).await.unwrap();
660            assert_eq!(response.status(), StatusCode::OK);
661        }
662    }
663
664    #[tokio::test]
665    async fn test_router_builder_default() {
666        let router = RouterBuilder::default().build();
667        let request = Request::builder()
668            .method(Method::GET)
669            .uri("/")
670            .body(Body::empty())
671            .unwrap();
672        let response = router.oneshot(request).await.unwrap();
673        assert_eq!(response.status(), StatusCode::NOT_FOUND);
674    }
675
676    // ====================================================================
677    // resource() RESTful 资源路由测试(1.2.5)
678    // ====================================================================
679
680    #[tokio::test]
681    async fn test_resource_all_seven_handlers() {
682        let routes = ResourceRoutes {
683            index: Some(axum::routing::get(|| async { "index" })),
684            create: Some(axum::routing::get(|| async { "create" })),
685            store: Some(axum::routing::post(|| async { "store" })),
686            show: Some(axum::routing::get(|| async { "show" })),
687            edit: Some(axum::routing::get(|| async { "edit" })),
688            update: Some(axum::routing::put(|| async { "update" })),
689            destroy: Some(axum::routing::delete(|| async { "destroy" })),
690        };
691        let router = resource("users", routes);
692
693        // GET /users → index
694        let req = Request::builder()
695            .method(Method::GET)
696            .uri("/users")
697            .body(Body::empty())
698            .unwrap();
699        let resp = router.clone().oneshot(req).await.unwrap();
700        assert_eq!(resp.status(), StatusCode::OK);
701
702        // POST /users → store
703        let req = Request::builder()
704            .method(Method::POST)
705            .uri("/users")
706            .body(Body::empty())
707            .unwrap();
708        let resp = router.clone().oneshot(req).await.unwrap();
709        assert_eq!(resp.status(), StatusCode::OK);
710
711        // GET /users/create → create
712        let req = Request::builder()
713            .method(Method::GET)
714            .uri("/users/create")
715            .body(Body::empty())
716            .unwrap();
717        let resp = router.clone().oneshot(req).await.unwrap();
718        assert_eq!(resp.status(), StatusCode::OK);
719
720        // GET /users/1 → show
721        let req = Request::builder()
722            .method(Method::GET)
723            .uri("/users/1")
724            .body(Body::empty())
725            .unwrap();
726        let resp = router.clone().oneshot(req).await.unwrap();
727        assert_eq!(resp.status(), StatusCode::OK);
728
729        // GET /users/1/edit → edit
730        let req = Request::builder()
731            .method(Method::GET)
732            .uri("/users/1/edit")
733            .body(Body::empty())
734            .unwrap();
735        let resp = router.clone().oneshot(req).await.unwrap();
736        assert_eq!(resp.status(), StatusCode::OK);
737
738        // PUT /users/1 → update
739        let req = Request::builder()
740            .method(Method::PUT)
741            .uri("/users/1")
742            .body(Body::empty())
743            .unwrap();
744        let resp = router.clone().oneshot(req).await.unwrap();
745        assert_eq!(resp.status(), StatusCode::OK);
746
747        // DELETE /users/1 → destroy
748        let req = Request::builder()
749            .method(Method::DELETE)
750            .uri("/users/1")
751            .body(Body::empty())
752            .unwrap();
753        let resp = router.oneshot(req).await.unwrap();
754        assert_eq!(resp.status(), StatusCode::OK);
755    }
756
757    #[tokio::test]
758    async fn test_resource_partial_handlers_only_index_and_store() {
759        let routes = ResourceRoutes {
760            index: Some(axum::routing::get(|| async { "list" })),
761            store: Some(axum::routing::post(|| async { "create" })),
762            ..Default::default()
763        };
764        let router = resource("articles", routes);
765
766        // GET /articles → 200
767        let req = Request::builder()
768            .method(Method::GET)
769            .uri("/articles")
770            .body(Body::empty())
771            .unwrap();
772        let resp = router.clone().oneshot(req).await.unwrap();
773        assert_eq!(resp.status(), StatusCode::OK);
774
775        // POST /articles → 200
776        let req = Request::builder()
777            .method(Method::POST)
778            .uri("/articles")
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 /articles/1 → 404(show 未注册)
785        let req = Request::builder()
786            .method(Method::GET)
787            .uri("/articles/1")
788            .body(Body::empty())
789            .unwrap();
790        let resp = router.clone().oneshot(req).await.unwrap();
791        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
792
793        // GET /articles/create → 404
794        let req = Request::builder()
795            .method(Method::GET)
796            .uri("/articles/create")
797            .body(Body::empty())
798            .unwrap();
799        let resp = router.oneshot(req).await.unwrap();
800        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
801    }
802
803    #[tokio::test]
804    async fn test_resource_only_id_routes() {
805        let routes = ResourceRoutes {
806            show: Some(axum::routing::get(|| async { "show" })),
807            update: Some(axum::routing::put(|| async { "update" })),
808            destroy: Some(axum::routing::delete(|| async { "destroy" })),
809            ..Default::default()
810        };
811        let router = resource("orders", routes);
812
813        // GET /orders → 404(index/store 未注册)
814        let req = Request::builder()
815            .method(Method::GET)
816            .uri("/orders")
817            .body(Body::empty())
818            .unwrap();
819        let resp = router.clone().oneshot(req).await.unwrap();
820        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
821
822        // GET /orders/1 → 200
823        let req = Request::builder()
824            .method(Method::GET)
825            .uri("/orders/1")
826            .body(Body::empty())
827            .unwrap();
828        let resp = router.clone().oneshot(req).await.unwrap();
829        assert_eq!(resp.status(), StatusCode::OK);
830
831        // PUT /orders/1 → 200
832        let req = Request::builder()
833            .method(Method::PUT)
834            .uri("/orders/1")
835            .body(Body::empty())
836            .unwrap();
837        let resp = router.clone().oneshot(req).await.unwrap();
838        assert_eq!(resp.status(), StatusCode::OK);
839
840        // DELETE /orders/1 → 200
841        let req = Request::builder()
842            .method(Method::DELETE)
843            .uri("/orders/1")
844            .body(Body::empty())
845            .unwrap();
846        let resp = router.oneshot(req).await.unwrap();
847        assert_eq!(resp.status(), StatusCode::OK);
848    }
849
850    #[tokio::test]
851    async fn test_resource_empty_routes() {
852        // 全部 None → 不注册任何路由 → 所有请求 404
853        let routes = ResourceRoutes::new();
854        let router = resource("widgets", routes);
855
856        let req = Request::builder()
857            .method(Method::GET)
858            .uri("/widgets")
859            .body(Body::empty())
860            .unwrap();
861        let resp = router.oneshot(req).await.unwrap();
862        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
863    }
864
865    #[tokio::test]
866    async fn test_resource_merged_into_router_builder() {
867        let routes = ResourceRoutes {
868            index: Some(axum::routing::get(|| async { "list" })),
869            store: Some(axum::routing::post(|| async { "create" })),
870            show: Some(axum::routing::get(|| async { "show" })),
871            ..Default::default()
872        };
873        let resource_router = resource("users", routes);
874
875        let router = RouterBuilder::new()
876            .merge(resource_router)
877            .get("/health", || async { "ok" })
878            .build();
879
880        // GET /health
881        let req = Request::builder()
882            .method(Method::GET)
883            .uri("/health")
884            .body(Body::empty())
885            .unwrap();
886        let resp = router.clone().oneshot(req).await.unwrap();
887        assert_eq!(resp.status(), StatusCode::OK);
888
889        // GET /users
890        let req = Request::builder()
891            .method(Method::GET)
892            .uri("/users")
893            .body(Body::empty())
894            .unwrap();
895        let resp = router.clone().oneshot(req).await.unwrap();
896        assert_eq!(resp.status(), StatusCode::OK);
897
898        // POST /users
899        let req = Request::builder()
900            .method(Method::POST)
901            .uri("/users")
902            .body(Body::empty())
903            .unwrap();
904        let resp = router.clone().oneshot(req).await.unwrap();
905        assert_eq!(resp.status(), StatusCode::OK);
906
907        // GET /users/42
908        let req = Request::builder()
909            .method(Method::GET)
910            .uri("/users/42")
911            .body(Body::empty())
912            .unwrap();
913        let resp = router.oneshot(req).await.unwrap();
914        assert_eq!(resp.status(), StatusCode::OK);
915    }
916
917    #[tokio::test]
918    async fn test_resource_body_content() {
919        let routes = ResourceRoutes {
920            index: Some(axum::routing::get(|| async { "user list" })),
921            ..Default::default()
922        };
923        let router = resource("users", routes);
924
925        let req = Request::builder()
926            .method(Method::GET)
927            .uri("/users")
928            .body(Body::empty())
929            .unwrap();
930        let resp = router.oneshot(req).await.unwrap();
931        assert_eq!(resp.status(), StatusCode::OK);
932        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
933        assert_eq!(&bytes[..], b"user list");
934    }
935}