1use axum::routing::{delete as route_delete, get, post, put};
28use axum::Router;
29
30use std::borrow::Cow;
31
32pub const APP_LIST: &[&str] = &["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"];
39
40pub const DENY_LIST: &[&str] = &["common"];
44
45pub const DEFAULT_APP: &str = "index";
47pub const DEFAULT_CONTROLLER: &str = "Index";
49pub const DEFAULT_ACTION: &str = "index";
51
52#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ParsedPath<'a> {
63 pub app: Cow<'a, str>,
65 pub controller: Cow<'a, str>,
67 pub action: Cow<'a, str>,
69}
70
71impl<'a> ParsedPath<'a> {
72 #[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 #[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#[inline]
112fn split_first_segment(s: &str) -> (Option<&str>, &str) {
113 let bytes = s.as_bytes();
114 let mut start = 0;
115
116 while start < bytes.len() && bytes[start] == b'/' {
118 start += 1;
119 }
120
121 if start >= bytes.len() {
122 return (None, "");
123 }
124
125 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#[inline]
166pub fn parse_path<'a>(uri: &'a str) -> ParsedPath<'a> {
167 let path = match crate::simd_str::find_separator_simd(uri.as_bytes(), b'?') {
169 Some(pos) => &uri[..pos],
170 None => uri,
171 };
172
173 let path = path.trim_start_matches('/');
175
176 if path.is_empty() {
178 return ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION);
179 }
180
181 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 (None, _, _) => ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION),
190 (Some(s0), None, _) => ParsedPath::new(DEFAULT_APP, capitalize_first(s0), DEFAULT_ACTION),
192 (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 (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#[inline]
216pub fn is_app_in_map(name: &str) -> bool {
217 APP_LIST.contains(&name) && !DENY_LIST.contains(&name)
218}
219
220#[inline]
233pub fn capitalize_first(s: &str) -> Cow<'_, str> {
234 crate::simd_str::capitalize_first_simd(s)
235}
236
237pub struct RouterBuilder<S = ()> {
273 inner: Router<S>,
274}
275
276impl<S> RouterBuilder<S>
277where
278 S: Clone + Send + Sync + 'static,
279{
280 pub fn new() -> Self {
282 Self {
283 inner: Router::new(),
284 }
285 }
286
287 pub fn with_router(inner: Router<S>) -> Self {
289 Self { inner }
290 }
291
292 pub fn with_state<S2>(self, state: S) -> RouterBuilder<S2> {
305 RouterBuilder {
306 inner: self.inner.with_state(state),
307 }
308 }
309
310 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 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 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 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 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 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 pub fn merge(self, other: Router<S>) -> Self {
396 Self {
397 inner: self.inner.merge(other),
398 }
399 }
400
401 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
413use std::convert::Infallible;
415
416#[derive(Default)]
454pub struct ResourceRoutes {
455 pub index: Option<axum::routing::MethodRouter>,
457 pub create: Option<axum::routing::MethodRouter>,
459 pub store: Option<axum::routing::MethodRouter>,
461 pub show: Option<axum::routing::MethodRouter>,
463 pub edit: Option<axum::routing::MethodRouter>,
465 pub update: Option<axum::routing::MethodRouter>,
467 pub destroy: Option<axum::routing::MethodRouter>,
469}
470
471impl ResourceRoutes {
472 pub fn new() -> Self {
474 Self::default()
475 }
476}
477
478pub 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 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 if let Some(h) = routes.create {
536 router = router.route(&create_path, h);
537 }
538
539 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 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 #[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 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 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 #[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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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}