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<S = ()> {
207 inner: Router<S>,
208}
209
210impl<S> RouterBuilder<S>
211where
212 S: Clone + Send + Sync + 'static,
213{
214 pub fn new() -> Self {
216 Self {
217 inner: Router::new(),
218 }
219 }
220
221 pub fn with_router(inner: Router<S>) -> Self {
223 Self { inner }
224 }
225
226 pub fn with_state<S2>(self, state: S) -> RouterBuilder<S2> {
239 RouterBuilder {
240 inner: self.inner.with_state(state),
241 }
242 }
243
244 pub fn get<H, T>(self, path: &str, handler: H) -> Self
246 where
247 H: axum::handler::Handler<T, S>,
248 T: 'static,
249 {
250 Self {
251 inner: self.inner.route(path, get(handler)),
252 }
253 }
254
255 pub fn post<H, T>(self, path: &str, handler: H) -> Self
257 where
258 H: axum::handler::Handler<T, S>,
259 T: 'static,
260 {
261 Self {
262 inner: self.inner.route(path, post(handler)),
263 }
264 }
265
266 pub fn put<H, T>(self, path: &str, handler: H) -> Self
268 where
269 H: axum::handler::Handler<T, S>,
270 T: 'static,
271 {
272 Self {
273 inner: self.inner.route(path, put(handler)),
274 }
275 }
276
277 pub fn delete<H, T>(self, path: &str, handler: H) -> Self
279 where
280 H: axum::handler::Handler<T, S>,
281 T: 'static,
282 {
283 Self {
284 inner: self.inner.route(path, route_delete(handler)),
285 }
286 }
287
288 pub fn ws<H: crate::websocket_route::WsHandler>(self, path: &str, handler: H) -> Self {
304 let mr: axum::routing::MethodRouter<()> =
305 crate::websocket_route::ws_handler(handler);
306 let mr_s: axum::routing::MethodRouter<S> = mr.with_state(());
307 Self {
308 inner: self.inner.route(path, mr_s),
309 }
310 }
311
312 pub fn layer<L>(self, layer: L) -> Self
316 where
317 L: tower::Layer<axum::routing::Route> + Clone + Send + Sync + 'static,
318 L::Service: tower::Service<axum::extract::Request> + Clone + Send + Sync + 'static,
319 <L::Service as tower::Service<axum::extract::Request>>::Response:
320 axum::response::IntoResponse + 'static,
321 <L::Service as tower::Service<axum::extract::Request>>::Error: Into<Infallible> + 'static,
322 <L::Service as tower::Service<axum::extract::Request>>::Future: Send + 'static,
323 {
324 Self {
325 inner: self.inner.layer(layer),
326 }
327 }
328
329 pub fn merge(self, other: Router<S>) -> Self {
331 Self {
332 inner: self.inner.merge(other),
333 }
334 }
335
336 pub fn build(self) -> Router<S> {
338 self.inner
339 }
340}
341
342impl Default for RouterBuilder {
343 fn default() -> Self {
344 Self::new()
345 }
346}
347
348use std::convert::Infallible;
350
351#[derive(Default)]
389pub struct ResourceRoutes {
390 pub index: Option<axum::routing::MethodRouter>,
392 pub create: Option<axum::routing::MethodRouter>,
394 pub store: Option<axum::routing::MethodRouter>,
396 pub show: Option<axum::routing::MethodRouter>,
398 pub edit: Option<axum::routing::MethodRouter>,
400 pub update: Option<axum::routing::MethodRouter>,
402 pub destroy: Option<axum::routing::MethodRouter>,
404}
405
406impl ResourceRoutes {
407 pub fn new() -> Self {
409 Self::default()
410 }
411}
412
413pub fn resource(name: &str, routes: ResourceRoutes) -> axum::Router {
447 let base = format!("/{name}");
448 let with_id = format!("/{name}/{{id}}");
449 let create_path = format!("/{name}/create");
450 let edit_path = format!("/{name}/{{id}}/edit");
451
452 let mut router = axum::Router::new();
453
454 let mut base_methods = axum::routing::MethodRouter::new();
456 let mut has_base = false;
457 if let Some(h) = routes.index {
458 base_methods = base_methods.merge(h);
459 has_base = true;
460 }
461 if let Some(h) = routes.store {
462 base_methods = base_methods.merge(h);
463 has_base = true;
464 }
465 if has_base {
466 router = router.route(&base, base_methods);
467 }
468
469 if let Some(h) = routes.create {
471 router = router.route(&create_path, h);
472 }
473
474 let mut id_methods = axum::routing::MethodRouter::new();
476 let mut has_id = false;
477 if let Some(h) = routes.show {
478 id_methods = id_methods.merge(h);
479 has_id = true;
480 }
481 if let Some(h) = routes.update {
482 id_methods = id_methods.merge(h);
483 has_id = true;
484 }
485 if let Some(h) = routes.destroy {
486 id_methods = id_methods.merge(h);
487 has_id = true;
488 }
489 if has_id {
490 router = router.route(&with_id, id_methods);
491 }
492
493 if let Some(h) = routes.edit {
495 router = router.route(&edit_path, h);
496 }
497
498 router
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504 use axum::body::Body;
505 use axum::http::{Method, Request, StatusCode};
506 use http_body_util::BodyExt;
507 use tower::ServiceExt;
508
509 #[test]
514 fn test_parse_path_root() {
515 let p = parse_path("/");
516 assert_eq!(p, ParsedPath::new("index", "Index", "index"));
517 }
518
519 #[test]
520 fn test_parse_path_empty() {
521 let p = parse_path("");
522 assert_eq!(p, ParsedPath::new("index", "Index", "index"));
523 }
524
525 #[test]
526 fn test_parse_path_single_segment() {
527 let p = parse_path("/customer");
528 assert_eq!(p, ParsedPath::new("index", "Customer", "index"));
529 }
530
531 #[test]
532 fn test_parse_path_two_segments_no_app() {
533 let p = parse_path("/customer/list");
534 assert_eq!(p, ParsedPath::new("index", "Customer", "list"));
535 }
536
537 #[test]
538 fn test_parse_path_three_segments_with_app() {
539 let p = parse_path("/oapc/customer/index");
540 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
541 }
542
543 #[test]
544 fn test_parse_path_app_in_map_two_segments() {
545 let p = parse_path("/admin/login");
546 assert_eq!(p, ParsedPath::new("admin", "Login", "index"));
547 }
548
549 #[test]
550 fn test_parse_path_all_seven_apps() {
551 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
552 let p = parse_path(&format!("/{app}/customer/index"));
553 assert_eq!(p, ParsedPath::new(app, "Customer", "index"));
554 }
555 }
556
557 #[test]
558 fn test_parse_path_deny_common_app() {
559 let p = parse_path("/common/customer/index");
561 assert_eq!(p, ParsedPath::new("index", "Common", "customer"));
562 }
563
564 #[test]
565 fn test_parse_path_with_query_string() {
566 let p = parse_path("/oapc/customer/index?id=1&page=2");
567 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
568 }
569
570 #[test]
571 fn test_parse_path_with_trailing_slash() {
572 let p = parse_path("/oapc/customer/index/");
573 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
574 }
575
576 #[test]
577 fn test_parse_path_double_slash() {
578 let p = parse_path("//oapc//customer//index");
579 assert_eq!(p, ParsedPath::new("oapc", "Customer", "index"));
580 }
581
582 #[test]
583 fn test_parse_path_capitalize_first_only() {
584 let p = parse_path("/customerList");
586 assert_eq!(p, ParsedPath::new("index", "CustomerList", "index"));
587 }
588
589 #[test]
590 fn test_is_app_in_map_all_seven() {
591 for app in ["oapc", "admin", "api", "farm", "oapi", "cashier", "scene"] {
592 assert!(is_app_in_map(app), "{app} should be in app_map");
593 }
594 }
595
596 #[test]
597 fn test_is_app_in_map_deny_common() {
598 assert!(!is_app_in_map("common"));
599 }
600
601 #[test]
602 fn test_is_app_in_map_unknown_app() {
603 assert!(!is_app_in_map("unknown"));
604 assert!(!is_app_in_map(""));
605 }
606
607 #[tokio::test]
612 async fn test_router_builder_get() {
613 let router = RouterBuilder::new()
614 .get("/ping", || async { "pong" })
615 .build();
616 let request = Request::builder()
617 .method(Method::GET)
618 .uri("/ping")
619 .body(Body::empty())
620 .unwrap();
621 let response = router.oneshot(request).await.unwrap();
622 assert_eq!(response.status(), StatusCode::OK);
623 let bytes = response.into_body().collect().await.unwrap().to_bytes();
624 assert_eq!(&bytes[..], b"pong");
625 }
626
627 #[tokio::test]
628 async fn test_router_builder_post() {
629 let router = RouterBuilder::new()
630 .post("/echo", |body: Body| async move {
631 let bytes = body
632 .collect()
633 .await
634 .map_err(|_| ())
635 .map(|b| b.to_bytes())
636 .unwrap_or_default();
637 String::from_utf8_lossy(&bytes).to_string()
638 })
639 .build();
640 let request = Request::builder()
641 .method(Method::POST)
642 .uri("/echo")
643 .body(Body::from("hello"))
644 .unwrap();
645 let response = router.oneshot(request).await.unwrap();
646 assert_eq!(response.status(), StatusCode::OK);
647 let bytes = response.into_body().collect().await.unwrap().to_bytes();
648 assert_eq!(&bytes[..], b"hello");
649 }
650
651 #[tokio::test]
652 async fn test_router_builder_multiple_methods() {
653 let router = RouterBuilder::new()
654 .get("/items", || async { "list" })
655 .post("/items", || async { "create" })
656 .put("/items/1", || async { "update" })
657 .delete("/items/1", || async { "delete" })
658 .build();
659
660 let req = Request::builder()
662 .method(Method::GET)
663 .uri("/items")
664 .body(Body::empty())
665 .unwrap();
666 let resp = router.clone().oneshot(req).await.unwrap();
667 assert_eq!(resp.status(), StatusCode::OK);
668
669 let req = Request::builder()
671 .method(Method::POST)
672 .uri("/items")
673 .body(Body::empty())
674 .unwrap();
675 let resp = router.clone().oneshot(req).await.unwrap();
676 assert_eq!(resp.status(), StatusCode::OK);
677
678 let req = Request::builder()
680 .method(Method::PUT)
681 .uri("/items/1")
682 .body(Body::empty())
683 .unwrap();
684 let resp = router.clone().oneshot(req).await.unwrap();
685 assert_eq!(resp.status(), StatusCode::OK);
686
687 let req = Request::builder()
689 .method(Method::DELETE)
690 .uri("/items/1")
691 .body(Body::empty())
692 .unwrap();
693 let resp = router.oneshot(req).await.unwrap();
694 assert_eq!(resp.status(), StatusCode::OK);
695 }
696
697 #[tokio::test]
698 async fn test_router_builder_not_found() {
699 let router = RouterBuilder::new()
700 .get("/ping", || async { "pong" })
701 .build();
702 let request = Request::builder()
703 .method(Method::GET)
704 .uri("/unknown")
705 .body(Body::empty())
706 .unwrap();
707 let response = router.oneshot(request).await.unwrap();
708 assert_eq!(response.status(), StatusCode::NOT_FOUND);
709 }
710
711 #[tokio::test]
712 async fn test_router_builder_merge() {
713 let r1 = RouterBuilder::new().get("/a", || async { "A" }).build();
714 let r2 = RouterBuilder::new().get("/b", || async { "B" }).build();
715 let router = RouterBuilder::new().merge(r1).merge(r2).build();
716
717 for path in ["/a", "/b"] {
718 let request = Request::builder()
719 .method(Method::GET)
720 .uri(path)
721 .body(Body::empty())
722 .unwrap();
723 let response = router.clone().oneshot(request).await.unwrap();
724 assert_eq!(response.status(), StatusCode::OK);
725 }
726 }
727
728 #[tokio::test]
729 async fn test_router_builder_default() {
730 let router = RouterBuilder::default().build();
731 let request = Request::builder()
732 .method(Method::GET)
733 .uri("/")
734 .body(Body::empty())
735 .unwrap();
736 let response = router.oneshot(request).await.unwrap();
737 assert_eq!(response.status(), StatusCode::NOT_FOUND);
738 }
739
740 #[tokio::test]
745 async fn test_resource_all_seven_handlers() {
746 let routes = ResourceRoutes {
747 index: Some(axum::routing::get(|| async { "index" })),
748 create: Some(axum::routing::get(|| async { "create" })),
749 store: Some(axum::routing::post(|| async { "store" })),
750 show: Some(axum::routing::get(|| async { "show" })),
751 edit: Some(axum::routing::get(|| async { "edit" })),
752 update: Some(axum::routing::put(|| async { "update" })),
753 destroy: Some(axum::routing::delete(|| async { "destroy" })),
754 };
755 let router = resource("users", routes);
756
757 let req = Request::builder()
759 .method(Method::GET)
760 .uri("/users")
761 .body(Body::empty())
762 .unwrap();
763 let resp = router.clone().oneshot(req).await.unwrap();
764 assert_eq!(resp.status(), StatusCode::OK);
765
766 let req = Request::builder()
768 .method(Method::POST)
769 .uri("/users")
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::GET)
778 .uri("/users/create")
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("/users/1")
788 .body(Body::empty())
789 .unwrap();
790 let resp = router.clone().oneshot(req).await.unwrap();
791 assert_eq!(resp.status(), StatusCode::OK);
792
793 let req = Request::builder()
795 .method(Method::GET)
796 .uri("/users/1/edit")
797 .body(Body::empty())
798 .unwrap();
799 let resp = router.clone().oneshot(req).await.unwrap();
800 assert_eq!(resp.status(), StatusCode::OK);
801
802 let req = Request::builder()
804 .method(Method::PUT)
805 .uri("/users/1")
806 .body(Body::empty())
807 .unwrap();
808 let resp = router.clone().oneshot(req).await.unwrap();
809 assert_eq!(resp.status(), StatusCode::OK);
810
811 let req = Request::builder()
813 .method(Method::DELETE)
814 .uri("/users/1")
815 .body(Body::empty())
816 .unwrap();
817 let resp = router.oneshot(req).await.unwrap();
818 assert_eq!(resp.status(), StatusCode::OK);
819 }
820
821 #[tokio::test]
822 async fn test_resource_partial_handlers_only_index_and_store() {
823 let routes = ResourceRoutes {
824 index: Some(axum::routing::get(|| async { "list" })),
825 store: Some(axum::routing::post(|| async { "create" })),
826 ..Default::default()
827 };
828 let router = resource("articles", routes);
829
830 let req = Request::builder()
832 .method(Method::GET)
833 .uri("/articles")
834 .body(Body::empty())
835 .unwrap();
836 let resp = router.clone().oneshot(req).await.unwrap();
837 assert_eq!(resp.status(), StatusCode::OK);
838
839 let req = Request::builder()
841 .method(Method::POST)
842 .uri("/articles")
843 .body(Body::empty())
844 .unwrap();
845 let resp = router.clone().oneshot(req).await.unwrap();
846 assert_eq!(resp.status(), StatusCode::OK);
847
848 let req = Request::builder()
850 .method(Method::GET)
851 .uri("/articles/1")
852 .body(Body::empty())
853 .unwrap();
854 let resp = router.clone().oneshot(req).await.unwrap();
855 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
856
857 let req = Request::builder()
859 .method(Method::GET)
860 .uri("/articles/create")
861 .body(Body::empty())
862 .unwrap();
863 let resp = router.oneshot(req).await.unwrap();
864 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
865 }
866
867 #[tokio::test]
868 async fn test_resource_only_id_routes() {
869 let routes = ResourceRoutes {
870 show: Some(axum::routing::get(|| async { "show" })),
871 update: Some(axum::routing::put(|| async { "update" })),
872 destroy: Some(axum::routing::delete(|| async { "destroy" })),
873 ..Default::default()
874 };
875 let router = resource("orders", routes);
876
877 let req = Request::builder()
879 .method(Method::GET)
880 .uri("/orders")
881 .body(Body::empty())
882 .unwrap();
883 let resp = router.clone().oneshot(req).await.unwrap();
884 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
885
886 let req = Request::builder()
888 .method(Method::GET)
889 .uri("/orders/1")
890 .body(Body::empty())
891 .unwrap();
892 let resp = router.clone().oneshot(req).await.unwrap();
893 assert_eq!(resp.status(), StatusCode::OK);
894
895 let req = Request::builder()
897 .method(Method::PUT)
898 .uri("/orders/1")
899 .body(Body::empty())
900 .unwrap();
901 let resp = router.clone().oneshot(req).await.unwrap();
902 assert_eq!(resp.status(), StatusCode::OK);
903
904 let req = Request::builder()
906 .method(Method::DELETE)
907 .uri("/orders/1")
908 .body(Body::empty())
909 .unwrap();
910 let resp = router.oneshot(req).await.unwrap();
911 assert_eq!(resp.status(), StatusCode::OK);
912 }
913
914 #[tokio::test]
915 async fn test_resource_empty_routes() {
916 let routes = ResourceRoutes::new();
918 let router = resource("widgets", routes);
919
920 let req = Request::builder()
921 .method(Method::GET)
922 .uri("/widgets")
923 .body(Body::empty())
924 .unwrap();
925 let resp = router.oneshot(req).await.unwrap();
926 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
927 }
928
929 #[tokio::test]
930 async fn test_resource_merged_into_router_builder() {
931 let routes = ResourceRoutes {
932 index: Some(axum::routing::get(|| async { "list" })),
933 store: Some(axum::routing::post(|| async { "create" })),
934 show: Some(axum::routing::get(|| async { "show" })),
935 ..Default::default()
936 };
937 let resource_router = resource("users", routes);
938
939 let router = RouterBuilder::new()
940 .merge(resource_router)
941 .get("/health", || async { "ok" })
942 .build();
943
944 let req = Request::builder()
946 .method(Method::GET)
947 .uri("/health")
948 .body(Body::empty())
949 .unwrap();
950 let resp = router.clone().oneshot(req).await.unwrap();
951 assert_eq!(resp.status(), StatusCode::OK);
952
953 let req = Request::builder()
955 .method(Method::GET)
956 .uri("/users")
957 .body(Body::empty())
958 .unwrap();
959 let resp = router.clone().oneshot(req).await.unwrap();
960 assert_eq!(resp.status(), StatusCode::OK);
961
962 let req = Request::builder()
964 .method(Method::POST)
965 .uri("/users")
966 .body(Body::empty())
967 .unwrap();
968 let resp = router.clone().oneshot(req).await.unwrap();
969 assert_eq!(resp.status(), StatusCode::OK);
970
971 let req = Request::builder()
973 .method(Method::GET)
974 .uri("/users/42")
975 .body(Body::empty())
976 .unwrap();
977 let resp = router.oneshot(req).await.unwrap();
978 assert_eq!(resp.status(), StatusCode::OK);
979 }
980
981 #[tokio::test]
982 async fn test_resource_body_content() {
983 let routes = ResourceRoutes {
984 index: Some(axum::routing::get(|| async { "user list" })),
985 ..Default::default()
986 };
987 let router = resource("users", routes);
988
989 let req = Request::builder()
990 .method(Method::GET)
991 .uri("/users")
992 .body(Body::empty())
993 .unwrap();
994 let resp = router.oneshot(req).await.unwrap();
995 assert_eq!(resp.status(), StatusCode::OK);
996 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
997 assert_eq!(&bytes[..], b"user list");
998 }
999}