From 969acbfd4d81e4f94c9b637c7542f476eae3a697 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Sun, 25 Aug 2024 21:59:21 +0300 Subject: [PATCH] Add utoipa_axum bindings This PR adds a new crate `utoipa-axum` which provides bindings between `axum` and `utoipa`. It aims to blend as much as possible to the existing philosophy of axum way of registering handlers. This commit introduces new `OpenApiRouter` what wraps `OpenApi` and axum `Router` which provides passthrough implementation for most of the axum Router methods and collects and combines the `OpenApi` from registered routes. Routes registred only via `routes!()` macro will get added to the `OpenApi`. Also this commit introduces `routes!()` macro which collects axum handlers annotated with `#[utoipa::path()]` attribute macro to single paths intance which is then provided to the `OpenApiRouter`. Example of supported sytanx. ```rust let user_router: OpenApiRouter = OpenApiRouter::new() .routes(routes!(search_user)) .routes(routes!(get_user, post_user, delete_user)); ``` Fixes #991 --- .github/workflows/build.yaml | 3 + scripts/doc.sh | 2 +- scripts/test.sh | 3 +- utoipa-axum/Cargo.toml | 10 +- utoipa-axum/src/lib.rs | 340 +++++++++------------- utoipa-axum/src/router.rs | 67 ++--- utoipa-gen/Cargo.toml | 1 - utoipa-gen/src/path.rs | 12 +- utoipa-gen/src/path/handler.rs | 75 ----- utoipa-gen/tests/path_derive_axum_test.rs | 15 - utoipa/Cargo.toml | 1 - utoipa/src/lib.rs | 5 + 12 files changed, 192 insertions(+), 342 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2eb9ad11..0b17a18d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,6 +25,7 @@ jobs: - utoipa-redoc - utoipa-rapidoc - utoipa-scalar + - utoipa-axum fail-fast: true runs-on: ubuntu-latest @@ -62,6 +63,8 @@ jobs: changes=true elif [[ "$change" == "utoipa-scalar" && "${{ matrix.crate }}" == "utoipa-scalar" && $changes == false ]]; then changes=true + elif [[ "$change" == "utoipa-axum" && "${{ matrix.crate }}" == "utoipa-axum" && $changes == false ]]; then + changes=true fi done < <(git diff --name-only ${{ github.sha }}~ ${{ github.sha }} | grep .rs | awk -F \/ '{print $1}') echo "${{ matrix.crate }} changes: $changes" diff --git a/scripts/doc.sh b/scripts/doc.sh index b9e69f18..4c7ae511 100755 --- a/scripts/doc.sh +++ b/scripts/doc.sh @@ -3,5 +3,5 @@ # Generate utoipa workspace docs cargo +nightly doc -Z unstable-options --workspace --no-deps \ - --features actix_extras,openapi_extensions,yaml,uuid,ulid,url,non_strict_integers,actix-web,axum,rocket,axum_handler \ + --features actix_extras,openapi_extensions,yaml,uuid,ulid,url,non_strict_integers,actix-web,axum,rocket \ --config 'build.rustdocflags = ["--cfg", "doc_cfg"]' diff --git a/scripts/test.sh b/scripts/test.sh index e9b19fa1..7f49c3a0 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -22,7 +22,6 @@ for crate in $crates; do $CARGO test -p utoipa-gen --test path_derive_rocket --features rocket_extras $CARGO test -p utoipa-gen --test path_derive_axum_test --features axum_extras - $CARGO test -p utoipa-gen --test path_derive_axum_test --features axum_extras,axum_handler $CARGO test -p utoipa-gen --test path_derive_auto_into_responses_axum --features axum_extras,utoipa/auto_into_responses elif [[ "$crate" == "utoipa-swagger-ui" ]]; then $CARGO test -p utoipa-swagger-ui --features actix-web,rocket,axum @@ -33,6 +32,6 @@ for crate in $crates; do elif [[ "$crate" == "utoipa-scalar" ]]; then $CARGO test -p utoipa-scalar --features actix-web,rocket,axum elif [[ "$crate" == "utoipa-axum" ]]; then - $CARGO test -p utoipa-axum + $CARGO test -p utoipa-axum --features debug,utoipa/debug fi done diff --git a/utoipa-axum/Cargo.toml b/utoipa-axum/Cargo.toml index af0165b6..d4d142ad 100644 --- a/utoipa-axum/Cargo.toml +++ b/utoipa-axum/Cargo.toml @@ -11,14 +11,20 @@ categories = ["web-programming"] authors = ["Juha Kukkonen "] rust-version.workspace = true +[features] +debug = [] + [dependencies] axum = { version = "0.7", default-features = false } -once_cell = "1" -utoipa = { version = "5.0.0-alpha.1", path = "../utoipa", default-features = false } +utoipa = { version = "5.0.0-alpha", path = "../utoipa", default-features = false } async-trait = "0.1" tower-service = "0.3" tower-layer = "0.3.2" +paste = "1.0" [package.metadata.docs.rs] features = [] rustdoc-args = ["--cfg", "doc_cfg"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] } diff --git a/utoipa-axum/src/lib.rs b/utoipa-axum/src/lib.rs index 191491d8..fbfb6e25 100644 --- a/utoipa-axum/src/lib.rs +++ b/utoipa-axum/src/lib.rs @@ -20,7 +20,8 @@ //! _**Use [`OpenApiRouter`][router] to collect handlers with _`#[utoipa::path]`_ macro to compose service and form OpenAPI spec.**_ //! //! ```rust -//! # use router::OpenApiRouter; +//! # use utoipa::OpenApi; +//! # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter}; //! #[derive(utoipa::ToSchema)] //! struct Todo { //! id: i32, @@ -30,21 +31,17 @@ //! #[openapi(components(schemas(Todo)))] //! struct Api; //! # #[utoipa::path(get, path = "/search")] -//! # fn search_user() {} +//! # async fn search_user() {} //! # #[utoipa::path(get, path = "")] -//! # fn get_user() {} +//! # async fn get_user() {} //! # #[utoipa::path(post, path = "")] -//! # fn post_user() {} +//! # async fn post_user() {} //! # #[utoipa::path(delete, path = "")] -//! # fn delete_user() {} +//! # async fn delete_user() {} //! //! let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi()) -//! .routes(get_path(search_user)) -//! .routes( -//! get_path(get_user) -//! .post_path(post_user) -//! .delete_path(delete_user), -//! ); +//! .routes(routes!(search_user)) +//! .routes(routes!(get_user, post_user, delete_user)); //! //! let api = router.to_openapi(); //! let axum_router: axum::Router = router.into(); @@ -54,203 +51,146 @@ pub mod router; -use std::convert::Infallible; +use core::panic; -use axum::handler::Handler; -use axum::routing; -use axum::routing::{MethodFilter, MethodRouter}; +use axum::routing::MethodFilter; +use utoipa::openapi::PathItemType; -use self::router::CURRENT_PATHS; - -/// Extension trait of [`axum::handler::Handler`] that allows it to know it's OpenAPI path. -pub trait UtoipaHandler: Handler -where - T: 'static, - S: Clone + Send + Sync + 'static, -{ - /// Get path e.g. "/api/health" and path item ([`utoipa::openapi::path::PathItem`]) of the handler. +/// Extends [`utoipa::openapi::path::PathItem`] by providing conversion methods to convert this +/// path item type to a [`axum::routing::MethodFilter`]. +pub trait PathItemExt { + /// Convert this path item type ot a [`axum::routing::MethodFilter`]. + /// + /// Method filter is used with handler registration on [`axum::routing::MethodRouter`]. /// - /// Path and path item is used to construct the OpenAPI spec's path section with help of - /// [`OpenApiRouter`][router]. + /// # Panics /// - /// [router]: ./router/struct.OpenApiRouter.html - fn get_path_and_item(&self) -> (String, utoipa::openapi::path::PathItem); + /// [`utoipa::openapi::path::PathItemType::Connect`] will panic because _`axum`_ does not have + /// `CONNECT` type [`axum::routing::MethodFilter`]. + fn to_method_filter(&self) -> MethodFilter; } -impl UtoipaHandler for P -where - P: axum::handler::Handler + utoipa::Path, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - fn get_path_and_item(&self) -> (String, utoipa::openapi::path::PathItem) { - let path = P::path(); - let item = P::path_item(); - - (path, item) +impl PathItemExt for PathItemType { + fn to_method_filter(&self) -> MethodFilter { + match self { + PathItemType::Get => MethodFilter::GET, + PathItemType::Put => MethodFilter::PUT, + PathItemType::Post => MethodFilter::POST, + PathItemType::Head => MethodFilter::HEAD, + PathItemType::Patch => MethodFilter::PATCH, + PathItemType::Trace => MethodFilter::TRACE, + PathItemType::Delete => MethodFilter::DELETE, + PathItemType::Options => MethodFilter::OPTIONS, + PathItemType::Connect => panic!( + "`CONNECT` not supported, axum does not have `MethodFilter` for connect requests" + ), + } } } -macro_rules! chain_handle { - ( $name:ident $method:ident) => { - fn $name(self, handler: H) -> Self { - let mut paths = CURRENT_PATHS.write().unwrap(); +/// re-export paste so users do not need to add the dependency. +#[doc(hidden)] +pub use paste::paste; - let (path, item) = handler.get_path_and_item(); +/// Collect axum handlers annotated with [`utoipa::path`] to [`router::UtoipaMethodRouter`]. +/// +/// [`routes`] macro will return [`router::UtoipaMethodRouter`] which contains an +/// [`axum::routing::MethodRouter`] and currenty registered paths. The output of this macro is +/// meant to be used together with [`router::OpenApiRouter`] which combines the paths and axum +/// routers to a single entity. +/// +/// Only handlers collected with [`routes`] macro will get registered to the OpenApi. +/// +/// # Panics +/// +/// Routes registered via [`routes`] macro or via `axum::routing::*` operations are bound to same +/// rules where only one one HTTP method can can be registered once per call. This means that the +/// following will produce runtime panic from axum code. +/// +/// ```rust,no_run +/// # use utoipa_axum::{routes, router::UtoipaMethodRouter}; +/// # use utoipa::path; +/// #[utoipa::path(get, path = "/search")] +/// async fn search_user() {} +/// +/// #[utoipa::path(get, path = "")] +/// async fn get_user() {} +/// +/// let _: UtoipaMethodRouter = routes!(get_user, search_user); +/// ``` +/// Since the _`axum`_ does not support method filter for `CONNECT` requests, using this macro with +/// handler having request method type `CONNET` `#[utoipa::path(connet, path = "")]` will panic at +/// runtime. +/// +/// # Examples +/// +/// _**Create new `OpenApiRouter` with `get_user` and `post_user` paths.**_ +/// ```rust +/// # use utoipa_axum::{routes, router::{OpenApiRouter, UtoipaMethodRouter}}; +/// # use utoipa::path; +/// #[utoipa::path(get, path = "")] +/// async fn get_user() {} +/// +/// #[utoipa::path(post, path = "")] +/// async fn post_user() {} +/// +/// let _: OpenApiRouter = OpenApiRouter::new().routes(routes!(get_user, post_user)); +/// ``` +#[macro_export] +macro_rules! routes { + ( $handler:ident $(, $tail:tt)* ) => { + { + use $crate::PathItemExt; + let mut paths = utoipa::openapi::path::Paths::new(); + let (path, item, types) = routes!(@resolve_types $handler); paths.add_path(path, item); - - self.on(MethodFilter::$method, handler) + #[allow(unused_mut)] + let mut method_router = types.into_iter().fold(axum::routing::MethodRouter::new(), |router, path_type| { + router.on(path_type.to_method_filter(), $handler) + }); + $( method_router = routes!( method_router: paths: $tail ); )* + (paths, method_router) } }; -} - -/// Extension trait of [`axum::routing::MethodRouter`] which adds _`utoipa`_ specific chainable -/// handler methods to the router. -/// -/// The added methods works the same way as the axum ones but allows -/// automatic handler collection to the [`utoipa::openapi::OpenApi`] specification. -pub trait UtoipaMethodRouterExt { - /// Chain an additional `DELETE` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn delete_path(self, handler: H) -> Self; - /// Chain an additional `GET` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn get_path(self, handler: H) -> Self; - /// Chain an additional `HEAD` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn head_path(self, handler: H) -> Self; - /// Chain an additional `OPTIONS` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn options_path(self, handler: H) -> Self; - /// Chain an additional `PATCH` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn patch_path(self, handler: H) -> Self; - /// Chain an additional `POST` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn post_path(self, handler: H) -> Self; - /// Chain an additional `PUT` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn put_path(self, handler: H) -> Self; - /// Chain an additional `TRACE` requests using [`UtoipaHandler`]. - /// - /// Using this insteand of axum routing alternative will allow automatic path collection for the - /// [`utoipa::openapi::OpenApi`]. - /// - /// Both the axum routing version and this can be used simulatenously but handlers registered with - /// axum version will not get collected to the OpenAPI. - fn trace_path(self, handler: H) -> Self; -} - -// routing::get -impl UtoipaMethodRouterExt for MethodRouter -where - H: UtoipaHandler, - T: 'static, - S: Clone + Send + Sync + 'static, -{ - chain_handle!(delete_path DELETE); - chain_handle!(get_path GET); - chain_handle!(head_path HEAD); - chain_handle!(options_path OPTIONS); - chain_handle!(patch_path PATCH); - chain_handle!(post_path POST); - chain_handle!(put_path PUT); - chain_handle!(trace_path TRACE); -} - -macro_rules! top_level_handle { - ( $name:ident $method:ident) => { - - #[doc = concat!("Route `", stringify!($method), "` requests to the given handler using [`UtoipaHandler`].")] - #[doc = ""] - #[doc = "Using this insteand of axum routing alternative will allow automatic path collection for the"] - #[doc = "[`utoipa::openapi::OpenApi`]."] - #[doc = ""] - #[doc = "Both the axum routing version and this can be used simulatenously but handlers registered with"] - #[doc = "axum version will not get collected to the OpenAPI."] - pub fn $name(handler: H) -> MethodRouter - where - H: UtoipaHandler, - T: 'static, - S: Clone + Send + Sync + 'static, + ( $router:ident: $paths:ident: $handler:ident $(, $tail:tt)* ) => { { - let mut paths = CURRENT_PATHS.write().unwrap(); - - let (path, item) = handler.get_path_and_item(); - paths.add_path(path, item); - - routing::on(MethodFilter::$method, handler) + let (path, item, types) = routes!(@resolve_types $handler); + $paths.add_path(path, item); + types.into_iter().fold($router, |router, path_type| { + router.on(path_type.to_method_filter(), $handler) + }) } }; + ( @resolve_types $handler:ident ) => { + { + use utoipa::{Path, __dev::{PathItemTypes, Tags}}; + $crate::paste! { + let path = [<__path_ $handler>]::path(); + let mut path_item = [<__path_ $handler>]::path_item(); + let types = [<__path_ $handler>]::path_item_types(); + let tags = [< __path_ $handler>]::tags(); + if !tags.is_empty() { + for (_, operation) in path_item.operations.iter_mut() { + let operation_tags = operation.tags.get_or_insert(Vec::new()); + operation_tags.extend(tags.iter().map(ToString::to_string)); + } + } + (path, path_item, types) + } + } + }; + ( ) => {}; } -top_level_handle!(delete_path DELETE); -top_level_handle!(get_path GET); -top_level_handle!(head_path HEAD); -top_level_handle!(options_path OPTIONS); -top_level_handle!(patch_path PATCH); -top_level_handle!(post_path POST); -top_level_handle!(put_path PUT); -top_level_handle!(trace_path TRACE); - #[cfg(test)] mod tests { - use std::marker::Send; - - use axum::extract::State; - use utoipa::OpenApi; - - use self::router::OpenApiRouter; - use super::*; + use axum::extract::State; + use router::*; #[utoipa::path(get, path = "/")] async fn root() {} - #[utoipa::path(post, path = "/test")] - async fn test() {} - - #[utoipa::path(post, path = "/health")] - async fn health_handler() {} - - #[utoipa::path(post, path = "/api/foo")] - async fn post_foo() {} - // --- user #[utoipa::path(get, path = "/")] @@ -282,31 +222,27 @@ mod tests { #[test] fn axum_router_nest_openapi_routes_compile() { - let user_router: OpenApiRouter = OpenApiRouter::new().routes(get_path(search_user)).routes( - get_path(get_user) - .post_path(post_user) - .delete_path(delete_user), - ); + let user_router: OpenApiRouter = OpenApiRouter::new() + .routes(routes!(search_user)) + .routes(routes!(get_user, post_user, delete_user)); let customer_router: OpenApiRouter = OpenApiRouter::new() - .routes( - get_path(get_customer) - .post_path(post_customer) - .delete_path(delete_customer), - ) - .routes(get_path(search_customer)) + .routes(routes!(get_customer, post_customer, delete_customer)) + .routes(routes!(search_customer)) .with_state(String::new()); let router = OpenApiRouter::new() .nest("/api/user", user_router) .nest("/api/customer", customer_router) - .route("/", get_path(root)); + .route("/", axum::routing::get(root)); let _ = router.get_openapi(); } #[test] fn openapi_router_with_openapi() { + use utoipa::OpenApi; + #[derive(utoipa::ToSchema)] #[allow(unused)] struct Todo { @@ -317,8 +253,8 @@ mod tests { struct Api; let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi()) - .routes(get_path(search_user)) - .routes(get_path(get_user)); + .routes(routes!(search_user)) + .routes(routes!(get_user)); let paths = router.to_openapi().paths; let expected_paths = utoipa::openapi::path::PathsBuilder::new() @@ -342,6 +278,8 @@ mod tests { #[test] fn openapi_router_nest_openapi() { + use utoipa::OpenApi; + #[derive(utoipa::ToSchema)] #[allow(unused)] struct Todo { @@ -351,11 +289,11 @@ mod tests { #[openapi(components(schemas(Todo)))] struct Api; - let router: OpenApiRouter = - OpenApiRouter::with_openapi(Api::openapi()).routes(get_path(search_user)); + let router: router::OpenApiRouter = + router::OpenApiRouter::with_openapi(Api::openapi()).routes(routes!(search_user)); - let customer_router: OpenApiRouter = OpenApiRouter::new() - .routes(get_path(get_customer)) + let customer_router: router::OpenApiRouter = router::OpenApiRouter::new() + .routes(routes!(get_customer)) .with_state(String::new()); let mut router = router.nest("/api/customer", customer_router); diff --git a/utoipa-axum/src/router.rs b/utoipa-axum/src/router.rs index b6b3a23e..bf42d514 100644 --- a/utoipa-axum/src/router.rs +++ b/utoipa-axum/src/router.rs @@ -1,24 +1,15 @@ //! Implements Router for composing handlers and collecting OpenAPI information. use std::collections::BTreeMap; use std::convert::Infallible; -use std::sync::RwLock; use axum::extract::Request; +use axum::handler::Handler; use axum::response::IntoResponse; use axum::routing::{MethodRouter, Route, RouterAsService}; use axum::Router; -use once_cell::sync::Lazy; use tower_layer::Layer; use tower_service::Service; -use crate::UtoipaHandler; - -/// Cache for current routes that will be register to the [`OpenApiRouter`] once the -/// [`OpenApiRouter::routes`] method is called. -#[doc(hidden)] -pub(crate) static CURRENT_PATHS: Lazy> = - once_cell::sync::Lazy::new(|| RwLock::new(utoipa::openapi::path::Paths::new())); - #[inline] fn colonized_params>(path: S) -> String where @@ -27,6 +18,17 @@ where String::from(path).replace('}', "").replace('{', ":") } +/// Wrapper type for [`utoipa::openapi::path::Paths`] and [`axum::routing::MethodRouter`]. +/// +/// This is used with [`OpenApiRouter::routes`] method to register current _`paths`_ to the +/// [`utoipa::openapi::OpenApi`] of [`OpenApiRouter`] instance. +/// +/// See [`routes`][routes] for usage. +/// +/// [routes]: ../macro.routes.html +pub type UtoipaMethodRouter = + (utoipa::openapi::path::Paths, axum::routing::MethodRouter); + /// A wrapper struct for [`axum::Router`] and [`utoipa::openapi::OpenApi`] for composing handlers /// and services with collecting OpenAPI information from the handlers. /// @@ -35,6 +37,7 @@ where /// implemented can be easily called after converting this router to [`axum::Router`] by /// [`Into::into`]. #[derive(Clone)] +#[cfg_attr(feature = "debug", derive(Debug))] pub struct OpenApiRouter(Router, utoipa::openapi::OpenApi); impl OpenApiRouter @@ -57,6 +60,7 @@ where /// /// _**Use derived [`utoipa::openapi::OpenApi`] as source for [`OpenApiRouter`].**_ /// ```rust + /// # use utoipa::OpenApi; /// # use utoipa_axum::router::OpenApiRouter; /// #[derive(utoipa::ToSchema)] /// struct Todo { @@ -66,16 +70,9 @@ where /// #[openapi(components(schemas(Todo)))] /// struct Api; /// - /// let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi()) + /// let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi()); /// ``` pub fn with_openapi(openapi: utoipa::openapi::OpenApi) -> Self { - let mut paths = CURRENT_PATHS - .write() - .expect("write CURRENT_PATHS lock poisoned"); - if !paths.paths.is_empty() { - paths.paths = BTreeMap::new(); - } - Self(Router::new(), openapi) } @@ -87,7 +84,7 @@ where /// Passthrough method for [`axum::Router::fallback`]. pub fn fallback(self, handler: H) -> Self where - H: UtoipaHandler, + H: Handler, T: 'static, { Self(self.0.fallback(handler), self.1) @@ -115,13 +112,13 @@ where Self(self.0.layer(layer), self.1) } - /// Register paths with [`utoipa::path`] attribute macro to `self`. Paths will be extended to - /// [`utoipa::openapi::OpenApi`] and routes will be added to the [`axum::Router`]. - pub fn routes(mut self, method_router: MethodRouter) -> Self { - let mut paths = CURRENT_PATHS - .write() - .expect("write CURRENT_PATHS lock poisoned"); - + /// Register [`UtoipaMethodRouter`] content created with [`routes`][routes] macro to `self`. + /// + /// Paths of the [`UtoipaMethodRouter`] will be extended to [`utoipa::openapi::OpenApi`] and + /// [`axum::router::MethodRouter`] will be added to the [`axum::Router`]. + /// + /// [routes]: ../macro.routes.html + pub fn routes(mut self, (mut paths, method_router): UtoipaMethodRouter) -> Self { let router = if paths.paths.len() == 1 { let first_entry = &paths.paths.first_entry(); let path = first_entry.as_ref().map(|path| path.key()); @@ -133,6 +130,7 @@ where self.0.route(&colonized_params(path), method_router) } else { paths.paths.iter().fold(self.0, |this, (path, _)| { + let path = if path.is_empty() { "/" } else { path }; this.route(&colonized_params(path), method_router.clone()) }) }; @@ -140,9 +138,6 @@ where // add current paths to the OpenApi self.1.paths.paths.extend(paths.paths.clone()); - // clear the already added routes - paths.paths = BTreeMap::new(); - Self(router, self.1) } @@ -184,15 +179,14 @@ where /// /// _**Nest two routers.**_ /// ```rust - /// # use utiopa_axum::router::OpenApiRouter; - /// # + /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter}; /// #[utoipa::path(get, path = "/search")] /// async fn search() {} /// /// let search_router = OpenApiRouter::new() - /// .routes(utoipa_axum::get(search)) + /// .routes(utoipa_axum::routes!(search)); /// - /// let router: OpenApiRouter::new() + /// let router: OpenApiRouter = OpenApiRouter::new() /// .nest("/api", search_router); /// ``` pub fn nest(mut self, path: &str, router: OpenApiRouter) -> Self { @@ -235,15 +229,14 @@ where /// /// _**Merge two routers.**_ /// ```rust - /// # use utiopa_axum::router::OpenApiRouter; - /// # + /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter}; /// #[utoipa::path(get, path = "/search")] /// async fn search() {} /// /// let search_router = OpenApiRouter::new() - /// .routes(utoipa_axum::get(search)) + /// .routes(utoipa_axum::routes!(search)); /// - /// let router: OpenApiRouter::new() + /// let router: OpenApiRouter = OpenApiRouter::new() /// .merge(search_router); /// ``` pub fn merge(mut self, router: OpenApiRouter) -> Self { diff --git a/utoipa-gen/Cargo.toml b/utoipa-gen/Cargo.toml index 9f9eab28..04d71905 100644 --- a/utoipa-gen/Cargo.toml +++ b/utoipa-gen/Cargo.toml @@ -51,7 +51,6 @@ uuid = ["dep:uuid"] ulid = ["dep:ulid"] url = ["dep:url"] axum_extras = ["regex", "syn/extra-traits"] -axum_handler = [] time = [] smallvec = [] repr = [] diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index fa43484e..6629daf3 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -25,23 +25,16 @@ mod request_body; pub mod response; mod status; -#[cfg(not(feature = "axum_handler"))] const PATH_STRUCT_PREFIX: &str = "__path_"; #[inline] pub fn format_path_ident(fn_name: Cow<'_, Ident>) -> Cow<'_, Ident> { - #[cfg(not(feature = "axum_handler"))] { Cow::Owned(quote::format_ident!( "{PATH_STRUCT_PREFIX}{}", fn_name.as_ref() )) } - - #[cfg(feature = "axum_handler")] - { - fn_name - } } #[derive(Default)] @@ -490,6 +483,11 @@ impl<'p> ToTokensDiagnostics for Path<'p> { #tags_list.into() } } + impl utoipa::__dev::PathItemTypes for #impl_for { + fn path_item_types() -> Vec { + [#path_operation].into() + } + } impl utoipa::Path for #impl_for { fn path() -> String { #path_with_context_path diff --git a/utoipa-gen/src/path/handler.rs b/utoipa-gen/src/path/handler.rs index 59173131..30fbd1ad 100644 --- a/utoipa-gen/src/path/handler.rs +++ b/utoipa-gen/src/path/handler.rs @@ -10,7 +10,6 @@ pub struct Handler<'p> { pub handler_fn: &'p ItemFn, } -#[cfg(not(feature = "axum_handler"))] impl<'p> ToTokensDiagnostics for Handler<'p> { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), crate::Diagnostics> { let ast_fn = &self.handler_fn; @@ -23,77 +22,3 @@ impl<'p> ToTokensDiagnostics for Handler<'p> { Ok(()) } } - -#[cfg(feature = "axum_handler")] -enum HandlerState { - Arg(proc_macro2::TokenStream), - Default, -} - -#[cfg(feature = "axum_handler")] -impl HandlerState { - fn into_state_tokens(self) -> (Option, proc_macro2::TokenStream) { - match self { - Self::Arg(tokens) => (None, tokens), - Self::Default => ( - Some(quote! {}), - quote! {S}, - ), - } - } -} - -#[cfg(feature = "axum_handler")] -impl<'p> ToTokensDiagnostics for Handler<'p> { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), crate::Diagnostics> { - let ast_fn = &self.handler_fn; - let path = as_tokens_or_diagnostics!(&self.path); - let fn_name = &ast_fn.sig.ident; - // TODO refactor the extension FnArg processing, now it is done twice for axum, is there a - // way to just do it once??? - // See lib.rs and ext/axum.rs - let fn_args = crate::ext::fn_arg::get_fn_args(&ast_fn.sig.inputs)?; - - let state = if let Some(arg) = fn_args - .into_iter() - .find(|fn_arg| fn_arg.ty.is("State")) - .and_then(|fn_arg| fn_arg.ty.path) - { - let args = arg - .segments - .first() - .map(|segment| &segment.arguments) - .and_then(|path_args| match path_args { - syn::PathArguments::AngleBracketed(arg) => Some(&arg.args), - _ => None, - }); - - use quote::ToTokens; - HandlerState::Arg(args.to_token_stream()) - } else { - HandlerState::Default - }; - let (generic, state) = state.into_state_tokens(); - - tokens.extend(quote! { - #path - - impl #generic axum::handler::Handler for #fn_name { - type Future = std::pin::Pin< - std::boxed::Box< - (dyn std::future::Future> - + std::marker::Send - + 'static), - >, - >; - - fn call(self, req: axum::extract::Request, state: #state) -> Self::Future { - #ast_fn - #fn_name.call(req, state) - } - } - }); - - Ok(()) - } -} diff --git a/utoipa-gen/tests/path_derive_axum_test.rs b/utoipa-gen/tests/path_derive_axum_test.rs index de82fee4..304dcdaa 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -752,18 +752,3 @@ fn derive_path_with_validation_attributes_axum() { config ); } - -#[test] -#[cfg(feature = "axum_handler")] -fn test_axum_handler_derive_state_compile() { - use axum::extract::State; - - #[utoipa::path(get, path = "/search")] - async fn search(State(_s): State<(String, i32)>) {} - - #[utoipa::path(get, path = "/search-no-state")] - async fn search_no_state() {} - - #[utoipa::path(get, path = "/get/{id}")] - async fn get_item_by_id(Path(_id): Path) {} -} diff --git a/utoipa/Cargo.toml b/utoipa/Cargo.toml index d127780f..8ed56cb3 100644 --- a/utoipa/Cargo.toml +++ b/utoipa/Cargo.toml @@ -26,7 +26,6 @@ debug = ["utoipa-gen/debug"] actix_extras = ["utoipa-gen/actix_extras"] rocket_extras = ["utoipa-gen/rocket_extras"] axum_extras = ["utoipa-gen/axum_extras"] -axum_handler = ["utoipa-gen/axum_handler"] chrono = ["utoipa-gen/chrono"] decimal = ["utoipa-gen/decimal"] decimal_float = ["utoipa-gen/decimal_float"] diff --git a/utoipa/src/lib.rs b/utoipa/src/lib.rs index 3d530660..85b0a4ee 100644 --- a/utoipa/src/lib.rs +++ b/utoipa/src/lib.rs @@ -940,6 +940,7 @@ pub trait ToResponse<'__r> { #[doc(hidden)] pub mod __dev { + use crate::openapi::PathItemType; use crate::{utoipa, OpenApi}; pub trait PathConfig { @@ -990,6 +991,10 @@ pub mod __dev { api } } + + pub trait PathItemTypes { + fn path_item_types() -> Vec; + } } #[cfg(test)]