1use axum::routing::{delete as route_delete, get, post, put};
28use axum::Router;
29use std::collections::HashSet;
30use std::sync::LazyLock;
31
32pub static APP_MAP: LazyLock<HashSet<&'static str>> =
36 LazyLock::new(|| HashSet::from(["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"]));
37
38pub static DENY_APP_LIST: LazyLock<HashSet<&'static str>> =
42 LazyLock::new(|| HashSet::from(["common"]));
43
44pub const DEFAULT_APP: &str = "index";
46pub const DEFAULT_CONTROLLER: &str = "Index";
48pub const DEFAULT_ACTION: &str = "index";
50
51#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ParsedPath {
62 pub app: String,
64 pub controller: String,
66 pub action: String,
68}
69
70impl ParsedPath {
71 pub fn new(
73 app: impl Into<String>,
74 controller: impl Into<String>,
75 action: impl Into<String>,
76 ) -> Self {
77 Self {
78 app: app.into(),
79 controller: controller.into(),
80 action: action.into(),
81 }
82 }
83}
84
85pub fn parse_path(uri: &str) -> ParsedPath {
103 let path = uri.split('?').next().unwrap_or(uri);
105
106 let path = path.trim_start_matches('/');
108
109 if path.is_empty() {
111 return ParsedPath::new(DEFAULT_APP, DEFAULT_CONTROLLER, DEFAULT_ACTION);
112 }
113
114 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
116
117 match segments.len() {
118 1 => ParsedPath::new(DEFAULT_APP, capitalize_first(segments[0]), DEFAULT_ACTION),
120 2 => {
122 if is_app_in_map(segments[0]) {
124 ParsedPath::new(segments[0], capitalize_first(segments[1]), DEFAULT_ACTION)
125 } else {
126 ParsedPath::new(
128 DEFAULT_APP,
129 capitalize_first(segments[0]),
130 segments[1].to_string(),
131 )
132 }
133 }
134 _ => {
136 if is_app_in_map(segments[0]) {
137 ParsedPath::new(
139 segments[0],
140 capitalize_first(segments[1]),
141 segments[2].to_string(),
142 )
143 } else {
144 ParsedPath::new(
146 DEFAULT_APP,
147 capitalize_first(segments[0]),
148 segments[1].to_string(),
149 )
150 }
151 }
152 }
153}
154
155pub fn is_app_in_map(name: &str) -> bool {
157 APP_MAP.contains(name) && !DENY_APP_LIST.contains(name)
158}
159
160fn capitalize_first(s: &str) -> String {
164 let mut chars = s.chars();
165 match chars.next() {
166 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
167 None => String::new(),
168 }
169}
170
171pub struct RouterBuilder {
188 inner: Router,
189}
190
191impl RouterBuilder {
192 pub fn new() -> Self {
194 Self {
195 inner: Router::new(),
196 }
197 }
198
199 pub fn with_router(router: Router) -> Self {
201 Self { inner: router }
202 }
203
204 pub fn get<H, T>(self, path: &str, handler: H) -> Self
206 where
207 H: axum::handler::Handler<T, ()>,
208 T: 'static,
209 {
210 Self {
211 inner: self.inner.route(path, get(handler)),
212 }
213 }
214
215 pub fn post<H, T>(self, path: &str, handler: H) -> Self
217 where
218 H: axum::handler::Handler<T, ()>,
219 T: 'static,
220 {
221 Self {
222 inner: self.inner.route(path, post(handler)),
223 }
224 }
225
226 pub fn put<H, T>(self, path: &str, handler: H) -> Self
228 where
229 H: axum::handler::Handler<T, ()>,
230 T: 'static,
231 {
232 Self {
233 inner: self.inner.route(path, put(handler)),
234 }
235 }
236
237 pub fn delete<H, T>(self, path: &str, handler: H) -> Self
239 where
240 H: axum::handler::Handler<T, ()>,
241 T: 'static,
242 {
243 Self {
244 inner: self.inner.route(path, route_delete(handler)),
245 }
246 }
247
248 pub fn layer<L>(self, layer: L) -> Self
252 where
253 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
254 L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
255 <L::Service as tower::Service<axum::extract::Request>>::Response:
256 axum::response::IntoResponse + 'static,
257 <L::Service as tower::Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
258 <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
259 {
260 Self {
261 inner: self.inner.layer(layer),
262 }
263 }
264
265 pub fn merge(self, other: Router) -> Self {
267 Self {
268 inner: self.inner.merge(other),
269 }
270 }
271
272 pub fn build(self) -> Router {
274 self.inner
275 }
276}
277
278impl Default for RouterBuilder {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284use std::convert::Infallible;
286
287#[derive(Default)]
325pub struct ResourceRoutes {
326 pub index: Option<axum::routing::MethodRouter>,
328 pub create: Option<axum::routing::MethodRouter>,
330 pub store: Option<axum::routing::MethodRouter>,
332 pub show: Option<axum::routing::MethodRouter>,
334 pub edit: Option<axum::routing::MethodRouter>,
336 pub update: Option<axum::routing::MethodRouter>,
338 pub destroy: Option<axum::routing::MethodRouter>,
340}
341
342impl ResourceRoutes {
343 pub fn new() -> Self {
345 Self::default()
346 }
347}
348
349pub fn resource(name: &str, routes: ResourceRoutes) -> axum::Router {
383 let base = format!("/{name}");
384 let with_id = format!("/{name}/{{id}}");
385 let create_path = format!("/{name}/create");
386 let edit_path = format!("/{name}/{{id}}/edit");
387
388 let mut router = axum::Router::new();
389
390 let mut base_methods = axum::routing::MethodRouter::new();
392 let mut has_base = false;
393 if let Some(h) = routes.index {
394 base_methods = base_methods.merge(h);
395 has_base = true;
396 }
397 if let Some(h) = routes.store {
398 base_methods = base_methods.merge(h);
399 has_base = true;
400 }
401 if has_base {
402 router = router.route(&base, base_methods);
403 }
404
405 if let Some(h) = routes.create {
407 router = router.route(&create_path, h);
408 }
409
410 let mut id_methods = axum::routing::MethodRouter::new();
412 let mut has_id = false;
413 if let Some(h) = routes.show {
414 id_methods = id_methods.merge(h);
415 has_id = true;
416 }
417 if let Some(h) = routes.update {
418 id_methods = id_methods.merge(h);
419 has_id = true;
420 }
421 if let Some(h) = routes.destroy {
422 id_methods = id_methods.merge(h);
423 has_id = true;
424 }
425 if has_id {
426 router = router.route(&with_id, id_methods);
427 }
428
429 if let Some(h) = routes.edit {
431 router = router.route(&edit_path, h);
432 }
433
434 router
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use axum::body::Body;
441 use axum::http::{Method, Request, StatusCode};
442 use http_body_util::BodyExt;
443 use tower::ServiceExt;
444
445 #[test]
450 fn test_parse_path_root() {
451 let p = parse_path("/");
452 assert_eq!(p, ParsedPath::new("index", "Index", "index"));
453 }
454
455 #[test]
456 fn test_parse_path_empty() {
457 let p = parse_path("");
458 assert_eq!(p, ParsedPath::new("index", "Index", "index"));
459 }
460
461 #[test]
462 fn test_parse_path_single_segment() {
463 let p = parse_path("/customer");
464 assert_eq!(p, ParsedPath::new("index", "Customer", "index"));
465 }
466
467 #[test]
468 fn test_parse_path_two_segments_no_app() {
469 let p = parse_path("/customer/list");
470 assert_eq!(p, ParsedPath::new("index", "Customer", "list"));
471 }
472
473 #[test]
474 fn test_parse_path_three_segments_with_app() {
475 let p = parse_path("/oapc/customer/index");
476 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
477 }
478
479 #[test]
480 fn test_parse_path_app_in_map_two_segments() {
481 let p = parse_path("/admin/login");
482 assert_eq!(p, ParsedPath::new("admin", "Login", "index"));
483 }
484
485 #[test]
486 fn test_parse_path_all_seven_apps() {
487 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
488 let p = parse_path(&format!("/{app}/customer/index"));
489 assert_eq!(p, ParsedPath::new(app, "Customer", "index"));
490 }
491 }
492
493 #[test]
494 fn test_parse_path_deny_common_app() {
495 let p = parse_path("/common/customer/index");
497 assert_eq!(p, ParsedPath::new("index", "Common", "customer"));
498 }
499
500 #[test]
501 fn test_parse_path_with_query_string() {
502 let p = parse_path("/oapc/customer/index?id=1&page=2");
503 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
504 }
505
506 #[test]
507 fn test_parse_path_with_trailing_slash() {
508 let p = parse_path("/oapc/customer/index/");
509 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
510 }
511
512 #[test]
513 fn test_parse_path_double_slash() {
514 let p = parse_path("//oapc//customer//index");
515 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
516 }
517
518 #[test]
519 fn test_parse_path_capitalize_first_only() {
520 let p = parse_path("/customerList");
522 assert_eq!(p, ParsedPath::new("index", "CustomerList", "index"));
523 }
524
525 #[test]
526 fn test_is_app_in_map_all_seven() {
527 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
528 assert!(is_app_in_map(app), "{app} should be in app_map");
529 }
530 }
531
532 #[test]
533 fn test_is_app_in_map_deny_common() {
534 assert!(!is_app_in_map("common"));
535 }
536
537 #[test]
538 fn test_is_app_in_map_unknown_app() {
539 assert!(!is_app_in_map("unknown"));
540 assert!(!is_app_in_map(""));
541 }
542
543 #[tokio::test]
548 async fn test_router_builder_get() {
549 let router = RouterBuilder::new()
550 .get("/ping", || async { "pong" })
551 .build();
552 let request = Request::builder()
553 .method(Method::GET)
554 .uri("/ping")
555 .body(Body::empty())
556 .unwrap();
557 let response = router.oneshot(request).await.unwrap();
558 assert_eq!(response.status(), StatusCode::OK);
559 let bytes = response.into_body().collect().await.unwrap().to_bytes();
560 assert_eq!(&bytes[..], b"pong");
561 }
562
563 #[tokio::test]
564 async fn test_router_builder_post() {
565 let router = RouterBuilder::new()
566 .post("/echo", |body: Body| async move {
567 let bytes = body
568 .collect()
569 .await
570 .map_err(|_| ())
571 .map(|b| b.to_bytes())
572 .unwrap_or_default();
573 String::from_utf8_lossy(&bytes).to_string()
574 })
575 .build();
576 let request = Request::builder()
577 .method(Method::POST)
578 .uri("/echo")
579 .body(Body::from("hello"))
580 .unwrap();
581 let response = router.oneshot(request).await.unwrap();
582 assert_eq!(response.status(), StatusCode::OK);
583 let bytes = response.into_body().collect().await.unwrap().to_bytes();
584 assert_eq!(&bytes[..], b"hello");
585 }
586
587 #[tokio::test]
588 async fn test_router_builder_multiple_methods() {
589 let router = RouterBuilder::new()
590 .get("/items", || async { "list" })
591 .post("/items", || async { "create" })
592 .put("/items/1", || async { "update" })
593 .delete("/items/1", || async { "delete" })
594 .build();
595
596 let req = Request::builder()
598 .method(Method::GET)
599 .uri("/items")
600 .body(Body::empty())
601 .unwrap();
602 let resp = router.clone().oneshot(req).await.unwrap();
603 assert_eq!(resp.status(), StatusCode::OK);
604
605 let req = Request::builder()
607 .method(Method::POST)
608 .uri("/items")
609 .body(Body::empty())
610 .unwrap();
611 let resp = router.clone().oneshot(req).await.unwrap();
612 assert_eq!(resp.status(), StatusCode::OK);
613
614 let req = Request::builder()
616 .method(Method::PUT)
617 .uri("/items/1")
618 .body(Body::empty())
619 .unwrap();
620 let resp = router.clone().oneshot(req).await.unwrap();
621 assert_eq!(resp.status(), StatusCode::OK);
622
623 let req = Request::builder()
625 .method(Method::DELETE)
626 .uri("/items/1")
627 .body(Body::empty())
628 .unwrap();
629 let resp = router.oneshot(req).await.unwrap();
630 assert_eq!(resp.status(), StatusCode::OK);
631 }
632
633 #[tokio::test]
634 async fn test_router_builder_not_found() {
635 let router = RouterBuilder::new()
636 .get("/ping", || async { "pong" })
637 .build();
638 let request = Request::builder()
639 .method(Method::GET)
640 .uri("/unknown")
641 .body(Body::empty())
642 .unwrap();
643 let response = router.oneshot(request).await.unwrap();
644 assert_eq!(response.status(), StatusCode::NOT_FOUND);
645 }
646
647 #[tokio::test]
648 async fn test_router_builder_merge() {
649 let r1 = RouterBuilder::new().get("/a", || async { "A" }).build();
650 let r2 = RouterBuilder::new().get("/b", || async { "B" }).build();
651 let router = RouterBuilder::new().merge(r1).merge(r2).build();
652
653 for path in ["/a", "/b"] {
654 let request = Request::builder()
655 .method(Method::GET)
656 .uri(path)
657 .body(Body::empty())
658 .unwrap();
659 let response = router.clone().oneshot(request).await.unwrap();
660 assert_eq!(response.status(), StatusCode::OK);
661 }
662 }
663
664 #[tokio::test]
665 async fn test_router_builder_default() {
666 let router = RouterBuilder::default().build();
667 let request = Request::builder()
668 .method(Method::GET)
669 .uri("/")
670 .body(Body::empty())
671 .unwrap();
672 let response = router.oneshot(request).await.unwrap();
673 assert_eq!(response.status(), StatusCode::NOT_FOUND);
674 }
675
676 #[tokio::test]
681 async fn test_resource_all_seven_handlers() {
682 let routes = ResourceRoutes {
683 index: Some(axum::routing::get(|| async { "index" })),
684 create: Some(axum::routing::get(|| async { "create" })),
685 store: Some(axum::routing::post(|| async { "store" })),
686 show: Some(axum::routing::get(|| async { "show" })),
687 edit: Some(axum::routing::get(|| async { "edit" })),
688 update: Some(axum::routing::put(|| async { "update" })),
689 destroy: Some(axum::routing::delete(|| async { "destroy" })),
690 };
691 let router = resource("users", routes);
692
693 let req = Request::builder()
695 .method(Method::GET)
696 .uri("/users")
697 .body(Body::empty())
698 .unwrap();
699 let resp = router.clone().oneshot(req).await.unwrap();
700 assert_eq!(resp.status(), StatusCode::OK);
701
702 let req = Request::builder()
704 .method(Method::POST)
705 .uri("/users")
706 .body(Body::empty())
707 .unwrap();
708 let resp = router.clone().oneshot(req).await.unwrap();
709 assert_eq!(resp.status(), StatusCode::OK);
710
711 let req = Request::builder()
713 .method(Method::GET)
714 .uri("/users/create")
715 .body(Body::empty())
716 .unwrap();
717 let resp = router.clone().oneshot(req).await.unwrap();
718 assert_eq!(resp.status(), StatusCode::OK);
719
720 let req = Request::builder()
722 .method(Method::GET)
723 .uri("/users/1")
724 .body(Body::empty())
725 .unwrap();
726 let resp = router.clone().oneshot(req).await.unwrap();
727 assert_eq!(resp.status(), StatusCode::OK);
728
729 let req = Request::builder()
731 .method(Method::GET)
732 .uri("/users/1/edit")
733 .body(Body::empty())
734 .unwrap();
735 let resp = router.clone().oneshot(req).await.unwrap();
736 assert_eq!(resp.status(), StatusCode::OK);
737
738 let req = Request::builder()
740 .method(Method::PUT)
741 .uri("/users/1")
742 .body(Body::empty())
743 .unwrap();
744 let resp = router.clone().oneshot(req).await.unwrap();
745 assert_eq!(resp.status(), StatusCode::OK);
746
747 let req = Request::builder()
749 .method(Method::DELETE)
750 .uri("/users/1")
751 .body(Body::empty())
752 .unwrap();
753 let resp = router.oneshot(req).await.unwrap();
754 assert_eq!(resp.status(), StatusCode::OK);
755 }
756
757 #[tokio::test]
758 async fn test_resource_partial_handlers_only_index_and_store() {
759 let routes = ResourceRoutes {
760 index: Some(axum::routing::get(|| async { "list" })),
761 store: Some(axum::routing::post(|| async { "create" })),
762 ..Default::default()
763 };
764 let router = resource("articles", routes);
765
766 let req = Request::builder()
768 .method(Method::GET)
769 .uri("/articles")
770 .body(Body::empty())
771 .unwrap();
772 let resp = router.clone().oneshot(req).await.unwrap();
773 assert_eq!(resp.status(), StatusCode::OK);
774
775 let req = Request::builder()
777 .method(Method::POST)
778 .uri("/articles")
779 .body(Body::empty())
780 .unwrap();
781 let resp = router.clone().oneshot(req).await.unwrap();
782 assert_eq!(resp.status(), StatusCode::OK);
783
784 let req = Request::builder()
786 .method(Method::GET)
787 .uri("/articles/1")
788 .body(Body::empty())
789 .unwrap();
790 let resp = router.clone().oneshot(req).await.unwrap();
791 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
792
793 let req = Request::builder()
795 .method(Method::GET)
796 .uri("/articles/create")
797 .body(Body::empty())
798 .unwrap();
799 let resp = router.oneshot(req).await.unwrap();
800 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
801 }
802
803 #[tokio::test]
804 async fn test_resource_only_id_routes() {
805 let routes = ResourceRoutes {
806 show: Some(axum::routing::get(|| async { "show" })),
807 update: Some(axum::routing::put(|| async { "update" })),
808 destroy: Some(axum::routing::delete(|| async { "destroy" })),
809 ..Default::default()
810 };
811 let router = resource("orders", routes);
812
813 let req = Request::builder()
815 .method(Method::GET)
816 .uri("/orders")
817 .body(Body::empty())
818 .unwrap();
819 let resp = router.clone().oneshot(req).await.unwrap();
820 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
821
822 let req = Request::builder()
824 .method(Method::GET)
825 .uri("/orders/1")
826 .body(Body::empty())
827 .unwrap();
828 let resp = router.clone().oneshot(req).await.unwrap();
829 assert_eq!(resp.status(), StatusCode::OK);
830
831 let req = Request::builder()
833 .method(Method::PUT)
834 .uri("/orders/1")
835 .body(Body::empty())
836 .unwrap();
837 let resp = router.clone().oneshot(req).await.unwrap();
838 assert_eq!(resp.status(), StatusCode::OK);
839
840 let req = Request::builder()
842 .method(Method::DELETE)
843 .uri("/orders/1")
844 .body(Body::empty())
845 .unwrap();
846 let resp = router.oneshot(req).await.unwrap();
847 assert_eq!(resp.status(), StatusCode::OK);
848 }
849
850 #[tokio::test]
851 async fn test_resource_empty_routes() {
852 let routes = ResourceRoutes::new();
854 let router = resource("widgets", routes);
855
856 let req = Request::builder()
857 .method(Method::GET)
858 .uri("/widgets")
859 .body(Body::empty())
860 .unwrap();
861 let resp = router.oneshot(req).await.unwrap();
862 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
863 }
864
865 #[tokio::test]
866 async fn test_resource_merged_into_router_builder() {
867 let routes = ResourceRoutes {
868 index: Some(axum::routing::get(|| async { "list" })),
869 store: Some(axum::routing::post(|| async { "create" })),
870 show: Some(axum::routing::get(|| async { "show" })),
871 ..Default::default()
872 };
873 let resource_router = resource("users", routes);
874
875 let router = RouterBuilder::new()
876 .merge(resource_router)
877 .get("/health", || async { "ok" })
878 .build();
879
880 let req = Request::builder()
882 .method(Method::GET)
883 .uri("/health")
884 .body(Body::empty())
885 .unwrap();
886 let resp = router.clone().oneshot(req).await.unwrap();
887 assert_eq!(resp.status(), StatusCode::OK);
888
889 let req = Request::builder()
891 .method(Method::GET)
892 .uri("/users")
893 .body(Body::empty())
894 .unwrap();
895 let resp = router.clone().oneshot(req).await.unwrap();
896 assert_eq!(resp.status(), StatusCode::OK);
897
898 let req = Request::builder()
900 .method(Method::POST)
901 .uri("/users")
902 .body(Body::empty())
903 .unwrap();
904 let resp = router.clone().oneshot(req).await.unwrap();
905 assert_eq!(resp.status(), StatusCode::OK);
906
907 let req = Request::builder()
909 .method(Method::GET)
910 .uri("/users/42")
911 .body(Body::empty())
912 .unwrap();
913 let resp = router.oneshot(req).await.unwrap();
914 assert_eq!(resp.status(), StatusCode::OK);
915 }
916
917 #[tokio::test]
918 async fn test_resource_body_content() {
919 let routes = ResourceRoutes {
920 index: Some(axum::routing::get(|| async { "user list" })),
921 ..Default::default()
922 };
923 let router = resource("users", routes);
924
925 let req = Request::builder()
926 .method(Method::GET)
927 .uri("/users")
928 .body(Body::empty())
929 .unwrap();
930 let resp = router.oneshot(req).await.unwrap();
931 assert_eq!(resp.status(), StatusCode::OK);
932 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
933 assert_eq!(&bytes[..], b"user list");
934 }
935}