Skip to main content

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