From 348835f18aa6172ed0978ce2a6ac4af7f1fef934 Mon Sep 17 00:00:00 2001 From: Denux Date: Mon, 3 Feb 2025 18:33:41 +0100 Subject: [PATCH] Added prefix and route nesting to AppRoutes - replaced snapshot testing with normal asserts --- src/controller/app_routes.rs | 257 ++++++++++++++---- ...troller__app_routes__tests__[[slash]].snap | 5 - ...__app_routes__tests__[[slash]_health].snap | 5 - ...er__app_routes__tests__[[slash]_ping].snap | 5 - ...routes__tests__[[slash]api[slash]bar].snap | 5 - ...routes__tests__[[slash]api[slash]foo].snap | 5 - ...es__tests__[[slash]api[slash]loco-rs].snap | 5 - ...outes__tests__[[slash]api[slash]loco].snap | 5 - ...app_routes__tests__[[slash]multiple1].snap | 5 - ...app_routes__tests__[[slash]multiple2].snap | 5 - ...app_routes__tests__[[slash]multiple3].snap | 5 - ...slash]normalizer[slash]foo[slash]bar].snap | 5 - ...slash]normalizer[slash]loco[slash]rs].snap | 5 - ...[slash]normalizer[slash]multiple-end].snap | 5 - ...lash]normalizer[slash]multiple-start].snap | 5 - ...s__[[slash]normalizer[slash]no-slash].snap | 5 - ...pp_routes__tests__[[slash]normalizer].snap | 5 - 17 files changed, 211 insertions(+), 126 deletions(-) delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_health].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_ping].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]bar].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]foo].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco-rs].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple1].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple2].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple3].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]foo[slash]bar].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]loco[slash]rs].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-end].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-start].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]no-slash].snap delete mode 100644 src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer].snap diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index d9d812ade..4e7b8d9e3 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -38,7 +38,7 @@ impl fmt::Display for ListRoutes { let actions_str = self .actions .iter() - .map(std::string::ToString::to_string) + .map(ToString::to_string) .collect::>() .join(","); @@ -68,16 +68,10 @@ impl AppRoutes { #[must_use] pub fn collect(&self) -> Vec { - let base_url_prefix = self - .get_prefix() - // add a leading slash forcefully. Axum routes must start with a leading slash. - // if we have double leading slashes - it will get normalized into a single slash later - .map_or("/".to_string(), |url| format!("/{}", url.as_str())); - self.get_routes() .iter() .flat_map(|controller| { - let mut uri_parts = vec![base_url_prefix.clone()]; + let mut uri_parts = vec![]; if let Some(prefix) = controller.prefix.as_ref() { uri_parts.push(prefix.to_string()); } @@ -87,15 +81,18 @@ impl AppRoutes { let joined_parts = parts.join("/"); let normalized = get_normalize_url().replace_all(&joined_parts, "/"); - let uri = if normalized == "/" { + let mut uri = if normalized == "/" { normalized.to_string() } else { - normalized.strip_suffix('/').map_or_else( - || normalized.to_string(), - std::string::ToString::to_string, - ) + normalized + .strip_suffix('/') + .map_or_else(|| normalized.to_string(), ToString::to_string) }; + if !uri.starts_with('/') { + uri.insert(0, '/'); + } + ListRoutes { uri, actions: handler.actions.clone(), @@ -132,14 +129,122 @@ impl AppRoutes { /// ``` #[must_use] pub fn prefix(mut self, prefix: &str) -> Self { - self.prefix = Some(prefix.to_string()); + let mut prefix = prefix.to_owned(); + if !prefix.ends_with('/') { + prefix.push('/'); + } + if !prefix.starts_with('/') { + prefix.insert(0, '/'); + } + + self.prefix = Some(prefix); + + self + } + + /// Set a nested prefix for the routes. This prefix will be appended to any existing prefix. + /// + /// # Example + /// + /// In the following example, you are adding `api` as a prefix and then nesting `v1` within it: + /// + /// ```rust + /// use loco_rs::controller::AppRoutes; + /// + /// let app_routes = AppRoutes::with_default_routes() + /// .prefix("api") + /// .nest_prefix("v1"); + /// + /// // This will result in routes with the prefix `/api/v1/` + /// ``` + #[must_use] + pub fn nest_prefix(mut self, prefix: &str) -> Self { + let prefix = self.prefix.as_ref().map_or_else( + || prefix.to_owned(), + |old_prefix| format!("{old_prefix}{prefix}"), + ); + self = self.prefix(&prefix); + + self + } + + /// Set a nested route with a prefix. This route will be added with the specified prefix. + /// The prefix will only be applied to the routes given in this function. + /// + /// # Example + /// + /// In the following example, you are adding `api` as a prefix and then nesting a route within it: + /// + /// ```rust, no_run + /// use loco_rs::controller::{AppRoutes, Routes}; + /// + /// let route = Routes::new().add("/notes", get(|| async { "notes" })); + /// let app_routes = AppRoutes::with_default_routes() + /// .prefix("api") + /// .nest_route("v1", route); + /// + /// // This will result in routes with the prefix `/api/v1/notes` + /// ``` + #[must_use] + pub fn nest_route(mut self, prefix: &str, route: Routes) -> Self { + let old_prefix = self.prefix.clone(); + self = self.nest_prefix(prefix); + self = self.add_route(route); + self.prefix = old_prefix; + + self + } + + /// Set multiple nested routes with a prefix. These routes will be added with the specified prefix. + /// The prefix will only be applied to the routes given in this function. + /// + /// # Example + /// + /// In the following example, you are adding `api` as a prefix and then nesting multiple routes within it: + /// + /// ```rust, no_run + /// use loco_rs::controller::{AppRoutes, Routes}; + /// + /// let routes = vec![ + /// Routes::new().add("/notes", get(|| async { "notes" })), + /// Routes::new().add("/users", get(|| async { "users" })), + /// ]; + /// let app_routes = AppRoutes::with_default_routes() + /// .prefix("api") + /// .nest_routes("v1", routes); + /// + /// // This will result in routes with the prefix `/api/v1/notes` and `/api/v1/users` + /// ``` + #[must_use] + pub fn nest_routes(mut self, prefix: &str, routes: Vec) -> Self { + let old_prefix = self.prefix.clone(); + self = self.nest_prefix(prefix); + self = self.add_routes(routes); + self.prefix = old_prefix; + self } /// Add a single route. #[must_use] - pub fn add_route(mut self, route: Routes) -> Self { + pub fn add_route(mut self, mut route: Routes) -> Self { + let routes_prefix = { + if let Some(mut prefix) = self.prefix.clone() { + let routes_prefix = route.prefix.clone().unwrap_or_default(); + + prefix.push_str(routes_prefix.as_str()); + Some(prefix) + } else { + route.prefix.clone() + } + }; + + if let Some(prefix) = routes_prefix { + route = route.prefix(prefix.as_str()); + } + self.routes.push(route); + self } @@ -147,8 +252,9 @@ impl AppRoutes { #[must_use] pub fn add_routes(mut self, mounts: Vec) -> Self { for mount in mounts { - self.routes.push(mount); + self = self.add_route(mount); } + self } @@ -206,13 +312,12 @@ impl AppRoutes { #[cfg(test)] mod tests { - - use insta::assert_debug_snapshot; - use rstest::rstest; - use tower::ServiceExt; - use super::*; use crate::{prelude::*, tests_cfg}; + use axum::http::Method; + use rstest::rstest; + use std::vec; + use tower::ServiceExt; async fn action() -> Result { format::json("loco") @@ -220,11 +325,16 @@ mod tests { #[test] fn can_load_app_route_from_default() { - for route in AppRoutes::with_default_routes().collect() { - assert_debug_snapshot!( - format!("[{}]", route.uri.replace('/', "[slash]")), - format!("{:?} {}", route.actions, route.uri) - ); + let routes = AppRoutes::with_default_routes().collect(); + let expected_routes = vec![ + ("/_ping", vec![Method::GET]), + ("/_health", vec![Method::GET]), + ]; + + assert_eq!(routes.len(), expected_routes.len()); + for (i, route) in routes.iter().enumerate() { + assert_eq!(route.uri, expected_routes[i.clone()].0); + assert_eq!(route.actions, expected_routes[i].1); } } @@ -253,11 +363,23 @@ mod tests { Routes::new().add("multiple3", patch(action)), ]); - for route in app_router.collect() { - assert_debug_snapshot!( - format!("[{}]", route.uri.replace('/', "[slash]")), - format!("{:?} {}", route.actions, route.uri) - ); + let routes = app_router.collect(); + let expected_routes = vec![ + ("/", vec![Method::GET]), + ("/normalizer/no-slash", vec![Method::GET]), + ("/normalizer", vec![Method::POST]), + ("/normalizer/loco/rs", vec![Method::DELETE]), + ("/normalizer/multiple-start", vec![Method::HEAD]), + ("/normalizer/multiple-end", vec![Method::TRACE]), + ("/multiple1", vec![Method::PUT]), + ("/multiple2", vec![Method::OPTIONS]), + ("/multiple3", vec![Method::PATCH]), + ]; + + assert_eq!(routes.len(), expected_routes.len()); + for (i, route) in routes.iter().enumerate() { + assert_eq!(route.uri, expected_routes[i.clone()].0); + assert_eq!(route.actions, expected_routes[i].1); } } @@ -271,27 +393,70 @@ mod tests { .prefix("api") .add_route(router_without_prefix); - for route in app_router.collect() { - assert_debug_snapshot!( - format!("[{}]", route.uri.replace('/', "[slash]")), - format!("{:?} {}", route.actions, route.uri) - ); + let routes = app_router.collect(); + let expected_routes = vec![ + ("/api/loco", vec![Method::GET]), + ("/api/loco-rs", vec![Method::GET]), + ]; + + assert_eq!(routes.len(), expected_routes.len()); + for (i, route) in routes.iter().enumerate() { + assert_eq!(route.uri, expected_routes[i.clone()].0); + assert_eq!(route.actions, expected_routes[i].1); + } + } + + #[test] + fn can_nest_prefix() { + let app_router = AppRoutes::empty().prefix("api").nest_prefix("v1"); + + assert_eq!(app_router.get_prefix().unwrap(), "/api/v1/"); + } + + #[test] + fn can_nest_route() { + let route = Routes::new().add("/notes", get(action)); + let app_router = AppRoutes::empty().prefix("api").nest_route("v1", route); + + let routes = app_router.collect(); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].uri, "/api/v1/notes"); + } + + #[test] + fn can_nest_routes() { + let routes = vec![ + Routes::new().add("/notes", get(action)), + Routes::new().add("/users", get(action)), + ]; + let app_router = AppRoutes::empty().prefix("api").nest_routes("v1", routes); + + let routes = app_router.collect(); + let expected_routes = vec![ + ("/api/v1/notes", vec![Method::GET]), + ("/api/v1/users", vec![Method::GET]), + ]; + + assert_eq!(routes.len(), expected_routes.len()); + for (i, route) in routes.iter().enumerate() { + assert_eq!(route.uri, expected_routes[i.clone()].0); + assert_eq!(route.actions, expected_routes[i].1); } } #[rstest] - #[case(axum::http::Method::GET, get(action))] - #[case(axum::http::Method::POST, post(action))] - #[case(axum::http::Method::DELETE, delete(action))] - #[case(axum::http::Method::HEAD, head(action))] - #[case(axum::http::Method::OPTIONS, options(action))] - #[case(axum::http::Method::PATCH, patch(action))] - #[case(axum::http::Method::POST, post(action))] - #[case(axum::http::Method::PUT, put(action))] - #[case(axum::http::Method::TRACE, trace(action))] + #[case(Method::GET, get(action))] + #[case(Method::POST, post(action))] + #[case(Method::DELETE, delete(action))] + #[case(Method::HEAD, head(action))] + #[case(Method::OPTIONS, options(action))] + #[case(Method::PATCH, patch(action))] + #[case(Method::POST, post(action))] + #[case(Method::PUT, put(action))] + #[case(Method::TRACE, trace(action))] #[tokio::test] async fn can_request_method( - #[case] http_method: axum::http::Method, + #[case] http_method: Method, #[case] method: axum::routing::MethodRouter, ) { let router_without_prefix = Routes::new().add("/loco", method); diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]].snap deleted file mode 100644 index c87c43b6d..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_health].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_health].snap deleted file mode 100644 index 5b9aafaee..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_health].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /_health" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_ping].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_ping].snap deleted file mode 100644 index 2e6f05aa0..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]_ping].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /_ping" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]bar].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]bar].snap deleted file mode 100644 index b65e4d39d..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]bar].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /api/bar" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]foo].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]foo].snap deleted file mode 100644 index 2f826466d..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]foo].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /api/foo" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco-rs].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco-rs].snap deleted file mode 100644 index e1e39891c..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco-rs].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /api/loco-rs" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco].snap deleted file mode 100644 index 4cfc803c5..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]api[slash]loco].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /api/loco" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple1].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple1].snap deleted file mode 100644 index 155bd6f2b..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple1].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[PUT] /multiple1" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple2].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple2].snap deleted file mode 100644 index 63e06dbd0..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple2].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[OPTIONS] /multiple2" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple3].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple3].snap deleted file mode 100644 index f64a40853..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]multiple3].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[PATCH] /multiple3" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]foo[slash]bar].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]foo[slash]bar].snap deleted file mode 100644 index d6ee9be92..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]foo[slash]bar].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[DELETE] /normalizer/foo/bar" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]loco[slash]rs].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]loco[slash]rs].snap deleted file mode 100644 index 1e5f17a66..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]loco[slash]rs].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[DELETE] /normalizer/loco/rs" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-end].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-end].snap deleted file mode 100644 index 06009cbde..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-end].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[TRACE] /normalizer/multiple-end" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-start].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-start].snap deleted file mode 100644 index b685d6fcb..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]multiple-start].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[HEAD] /normalizer/multiple-start" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]no-slash].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]no-slash].snap deleted file mode 100644 index c65a0d844..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer[slash]no-slash].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[GET] /normalizer/no-slash" diff --git a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer].snap b/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer].snap deleted file mode 100644 index 8c1f9c7c5..000000000 --- a/src/controller/snapshots/loco_rs__controller__app_routes__tests__[[slash]normalizer].snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: src/controller/app_routes.rs -expression: "format!(\"{:?} {}\", route.actions, route.uri)" ---- -"[POST] /normalizer"