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