diff --git a/utoipa-axum/CHANGELOG.md b/utoipa-axum/CHANGELOG.md index f9f07097..7cbb080c 100644 --- a/utoipa-axum/CHANGELOG.md +++ b/utoipa-axum/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog - utoipa-axum +## Unreleased + +### Fixed + +* Fix diverging axum route and openapi spec (https://github.com/juhaku/utoipa/pull/1199) + ## 0.1.2 - Oct 29 2024 ### Changed diff --git a/utoipa-axum/src/lib.rs b/utoipa-axum/src/lib.rs index 2d9cf711..7be427de 100644 --- a/utoipa-axum/src/lib.rs +++ b/utoipa-axum/src/lib.rs @@ -313,7 +313,7 @@ mod tests { let paths = router.to_openapi().paths; let expected_paths = utoipa::openapi::path::PathsBuilder::new() .path( - "/api/customer/", + "/api/customer", utoipa::openapi::PathItem::new( utoipa::openapi::path::HttpMethod::Get, utoipa::openapi::path::OperationBuilder::new() diff --git a/utoipa-axum/src/router.rs b/utoipa-axum/src/router.rs index 31533e0a..48aa76b6 100644 --- a/utoipa-axum/src/router.rs +++ b/utoipa-axum/src/router.rs @@ -320,8 +320,26 @@ where /// .nest("/api", search_router); /// ``` pub fn nest(self, path: &str, router: OpenApiRouter) -> Self { - let api = self.1.nest(path_template(path), router.1); - let path = if path.is_empty() { "/" } else { path }; + // from axum::routing::path_router::path_for_nested_route + // method is private, so we need to replicate it here + fn path_for_nested_route<'a>(prefix: &'a str, path: &'a str) -> String { + debug_assert!(prefix.starts_with('/')); + debug_assert!(path.starts_with('/')); + + if prefix.ends_with('/') { + format!("{prefix}{}", path.trim_start_matches('/')).into() + } else if path == "/" { + prefix.into() + } else { + format!("{prefix}{path}").into() + } + } + + let api = self.1.nest_with_path_composer( + path_for_nested_route(path, "/"), + router.1, + |a: &str, b: &str| path_for_nested_route(a, b), + ); let router = self.0.nest(&colonized_params(path), router.0); Self(router, api) diff --git a/utoipa/CHANGELOG.md b/utoipa/CHANGELOG.md index 04098a79..20bec0e5 100644 --- a/utoipa/CHANGELOG.md +++ b/utoipa/CHANGELOG.md @@ -3,6 +3,12 @@ **`utoipa`** is in direct correlation with **`utoipa-gen`** ([CHANGELOG.md](../utoipa-gen/CHANGELOG.md)). You might want to look into changes introduced to **`utoipa-gen`**. +## Unreleased + +### Fixed + +* Fix diverging axum route and openapi spec (https://github.com/juhaku/utoipa/pull/1199) + ## 5.2.0 - Nov 2024 ### Changed diff --git a/utoipa/src/openapi.rs b/utoipa/src/openapi.rs index ce8520bb..e2fdc19d 100644 --- a/utoipa/src/openapi.rs +++ b/utoipa/src/openapi.rs @@ -279,7 +279,26 @@ impl OpenApi { /// .build(); /// let nested = api.nest("/api/v1/user", user_api); /// ``` - pub fn nest, O: Into>(mut self, path: P, other: O) -> Self { + pub fn nest, O: Into>(self, path: P, other: O) -> Self { + self.nest_with_path_composer(path, other, |base, path| format!("{base}{path}")) + } + + /// Nest `other` [`OpenApi`] with custom path composer. + /// + /// In most cases you should use [`OpenApi::nest`] instead. + /// Only use this method if you need custom path composition for a specific use case. + /// + /// `composer` is a function that takes two strings, the base path and the path to nest, and returns the composed path for the API Specification. + pub fn nest_with_path_composer< + P: Into, + O: Into, + F: Fn(&str, &str) -> String, + >( + mut self, + path: P, + other: O, + composer: F, + ) -> Self { let path: String = path.into(); let mut other_api: OpenApi = other.into(); @@ -288,7 +307,7 @@ impl OpenApi { .paths .into_iter() .map(|(item_path, item)| { - let path = format!("{path}{item_path}"); + let path = composer(&path, &item_path); (path, item) }) .collect::>();