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 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 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
105pub fn parse_path<'a>(uri: &'a str) -> ParsedPath<'a> {
138 let path = uri.split('?').next().unwrap_or(uri);
140
141 let path = path.trim_start_matches('/');
143
144 if path.is_empty() {
146 return ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION);
147 }
148
149 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 (None, _, _) => ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION),
158 (Some(s0), None, _) => ParsedPath::new(DEFAULT_APP, capitalize_first(s0), DEFAULT_ACTION),
160 (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 (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
179pub fn is_app_in_map(name: &str) -> bool {
184 APP_LIST.contains(&name) && !DENY_LIST.contains(&name)
185}
186
187pub 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
232pub struct RouterBuilder<S = ()> {
268 inner: Router<S>,
269}
270
271impl<S> RouterBuilder<S>
272where
273 S: Clone + Send + Sync + 'static,
274{
275 pub fn new() -> Self {
277 Self {
278 inner: Router::new(),
279 }
280 }
281
282 pub fn with_router(inner: Router<S>) -> Self {
284 Self { inner }
285 }
286
287 pub fn with_state<S2>(self, state: S) -> RouterBuilder<S2> {
300 RouterBuilder {
301 inner: self.inner.with_state(state),
302 }
303 }
304
305 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 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 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 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 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 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 pub fn merge(self, other: Router<S>) -> Self {
391 Self {
392 inner: self.inner.merge(other),
393 }
394 }
395
396 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
408use std::convert::Infallible;
410
411#[derive(Default)]
449pub struct ResourceRoutes {
450 pub index: Option<axum::routing::MethodRouter>,
452 pub create: Option<axum::routing::MethodRouter>,
454 pub store: Option<axum::routing::MethodRouter>,
456 pub show: Option<axum::routing::MethodRouter>,
458 pub edit: Option<axum::routing::MethodRouter>,
460 pub update: Option<axum::routing::MethodRouter>,
462 pub destroy: Option<axum::routing::MethodRouter>,
464}
465
466impl ResourceRoutes {
467 pub fn new() -> Self {
469 Self::default()
470 }
471}
472
473pub 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 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 if let Some(h) = routes.create {
531 router = router.route(&create_path, h);
532 }
533
534 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 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 #[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 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 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 #[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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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}