From a0c8b1d267bf79a0333edbd6a4669faa11738929 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 `utiopa-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. Fixes #991 --- scripts/doc.sh | 2 +- scripts/test.sh | 3 +- utoipa-axum/Cargo.toml | 10 +- utoipa-axum/src/lib.rs | 329 +++++++++------------- utoipa-axum/src/router.rs | 58 ++-- utoipa-gen/Cargo.toml | 1 - utoipa-gen/src/path.rs | 5 + utoipa-gen/src/path/handler.rs | 75 ----- utoipa-gen/tests/path_derive_axum_test.rs | 14 - utoipa/Cargo.toml | 1 - utoipa/src/lib.rs | 5 + 11 files changed, 170 insertions(+), 333 deletions(-) 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..21eea038 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,133 @@ 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(); - - 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. +/// +/// # 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}; + ::paste::paste! { + let path = [<__path_ $handler>]::path(); + let path_item = [<__path_ $handler>]::path_item(); + let types = [<__path_ $handler>]::path_item_types(); + (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 +209,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 +240,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 +265,8 @@ mod tests { #[test] fn openapi_router_nest_openapi() { + use utoipa::OpenApi; + #[derive(utoipa::ToSchema)] #[allow(unused)] struct Todo { @@ -351,11 +276,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..39d466cd 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) @@ -117,11 +114,7 @@ where /// 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"); - + 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()); @@ -140,9 +133,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 +174,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 +224,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..d9bde664 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -490,6 +490,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..1ee18f27 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -753,17 +753,3 @@ fn derive_path_with_validation_attributes_axum() { ); } -#[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)]