From 16de786abeb99514d4e408e98693be7cd5b5ee1a Mon Sep 17 00:00:00 2001 From: tamasfe Date: Tue, 28 Nov 2023 14:02:51 +0100 Subject: [PATCH 1/2] feat(aide, axum-jsonschema, aide-axum-sqlx-tx): axum 0.7 support --- Cargo.toml | 1 + crates/aide-axum-sqlx-tx/Cargo.toml | 4 +- crates/aide/Cargo.toml | 16 +-- crates/aide/src/axum/inputs.rs | 12 +- crates/aide/src/axum/mod.rs | 172 +++++++++++++--------------- crates/aide/src/axum/routing.rs | 111 +++++------------- crates/aide/src/helpers/no_api.rs | 13 +-- crates/aide/src/helpers/use_api.rs | 88 +++++++------- crates/aide/src/helpers/with_api.rs | 21 ++-- crates/aide/src/redoc/mod.rs | 31 +++-- crates/axum-jsonschema/Cargo.toml | 12 +- crates/axum-jsonschema/src/lib.rs | 9 +- examples/example-axum/Cargo.toml | 6 +- examples/example-axum/src/main.rs | 8 +- 14 files changed, 218 insertions(+), 286 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c79c053..b01bb80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,3 @@ [workspace] members = ["crates/*", "examples/*"] +resolver = "2" diff --git a/crates/aide-axum-sqlx-tx/Cargo.toml b/crates/aide-axum-sqlx-tx/Cargo.toml index 5ff6da4..96ac738 100644 --- a/crates/aide-axum-sqlx-tx/Cargo.toml +++ b/crates/aide-axum-sqlx-tx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aide-axum-sqlx-tx" -version = "0.12.0" +version = "0.13.0" edition = "2021" resolver = "2" authors = ["Wicpar"] @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] sqlx-06 = { package = "axum-sqlx-tx", version = "0.5.0", optional = true } sqlx-07 = { package = "axum-sqlx-tx", version = "0.6.0", optional = true } -aide = { version = "0.12.0", path = "../aide" } +aide = { version = "0.13.0", path = "../aide" } [dev-dependencies] sqlx = { version = "^0.7", features = ["postgres"] } diff --git a/crates/aide/Cargo.toml b/crates/aide/Cargo.toml index 2a865af..0683a1d 100644 --- a/crates/aide/Cargo.toml +++ b/crates/aide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aide" -version = "0.12.0" +version = "0.13.0" authors = ["tamasfe"] edition = "2021" keywords = ["generate", "api", "openapi", "documentation", "specification"] @@ -10,8 +10,8 @@ description = "A code-first API documentation library" readme = "README.md" [dependencies] -indexmap = { version = "1", features = ["serde"] } -schemars = { version = "0.8", features = ["impl_json_schema", "indexmap"] } +indexmap = { version = "2.1", features = ["serde"] } +schemars = { version = "0.8.16", features = ["impl_json_schema", "indexmap2"] } serde = "1" serde_json = "1" thiserror = "1" @@ -20,10 +20,10 @@ aide-macros = { version = "0.7.0", path = "../aide-macros", optional = true } derive_more = "0.99.17" bytes = { version = "1", optional = true } -http = { version = "0.2", optional = true } +http = { version = "1.0.0", optional = true } -axum = { version = "0.6.0", optional = true } -axum-extra = { version = "0.7.3", optional = true } +axum = { version = "0.7.1", optional = true } +axum-extra = { version = "0.9.0", optional = true } tower-layer = { version = "0.3", optional = true } tower-service = { version = "0.3", optional = true } cfg-if = "1.0.0" @@ -31,7 +31,7 @@ cfg-if = "1.0.0" # custom axum extractors serde_qs = { version = "0.12.0", optional = true } -jwt-authorizer = { version = "0.10", default-features = false, optional = true } +jwt-authorizer = { version = "0.13", default-features = false, optional = true } [features] macros = ["dep:aide-macros"] @@ -39,7 +39,7 @@ redoc = [] skip_serializing_defaults = [] axum = ["dep:axum", "bytes", "http", "dep:tower-layer", "dep:tower-service", "serde_qs?/axum"] -axum-headers = ["axum/headers"] +# axum-headers = ["axum/headers"] axum-ws = ["axum/ws"] axum-multipart = ["axum/multipart"] axum-extra = ["axum", "dep:axum-extra"] diff --git a/crates/aide/src/axum/inputs.rs b/crates/aide/src/axum/inputs.rs index 5fe179a..2ec0e7d 100644 --- a/crates/aide/src/axum/inputs.rs +++ b/crates/aide/src/axum/inputs.rs @@ -6,9 +6,12 @@ use crate::{ }, operation::{add_parameters, set_body}, }; -use axum::extract::{ - BodyStream, ConnectInfo, Extension, Form, Host, Json, MatchedPath, OriginalUri, Path, Query, - RawBody, RawQuery, State, +use axum::{ + body::Body, + extract::{ + ConnectInfo, Extension, Form, Host, Json, MatchedPath, OriginalUri, Path, Query, RawQuery, + State, + }, }; use indexmap::IndexMap; use schemars::{ @@ -24,11 +27,10 @@ use crate::{ impl OperationInput for Extension {} impl OperationInput for State {} -impl OperationInput for BodyStream {} impl OperationInput for ConnectInfo {} impl OperationInput for MatchedPath {} impl OperationInput for OriginalUri {} -impl OperationInput for RawBody {} +impl OperationInput for Body {} impl OperationInput for RawQuery {} impl OperationInput for Host {} diff --git a/crates/aide/src/axum/mod.rs b/crates/aide/src/axum/mod.rs index 7cc6a87..67f7b97 100644 --- a/crates/aide/src/axum/mod.rs +++ b/crates/aide/src/axum/mod.rs @@ -29,10 +29,9 @@ //! async fn main() { //! let app = Router::new().route("/hello", post(hello_user)); //! -//! axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) -//! .serve(app.into_make_service()) -//! .await -//! .unwrap(); +//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); +//! +//! axum::serve(listener, app).await.unwrap(); //! } //! ``` //! @@ -86,17 +85,19 @@ //! ..OpenApi::default() //! }; //! -//! axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) -//! .serve( -//! app -//! // Generate the documentation. -//! .finish_api(&mut api) -//! // Expose the documentation to the handlers. -//! .layer(Extension(api)) -//! .into_make_service(), -//! ) -//! .await -//! .unwrap(); +//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); +//! +//! axum::serve( +//! listener, +//! app +//! // Generate the documentation. +//! .finish_api(&mut api) +//! // Expose the documentation to the handlers. +//! .layer(Extension(api)) +//! .into_make_service(), +//! ) +//! .await +//! .unwrap(); //! } //! ``` //! @@ -177,7 +178,7 @@ use crate::{ OperationInput, OperationOutput, }; use axum::{ - body::{Body, HttpBody}, + body::Body, extract::connect_info::IntoMakeServiceWithConnectInfo, handler::Handler, http::Request, @@ -206,12 +207,12 @@ pub mod routing; /// API documentation-specific features. #[must_use] #[derive(Debug)] -pub struct ApiRouter { +pub struct ApiRouter { paths: IndexMap, - router: Router, + router: Router, } -impl Clone for ApiRouter { +impl Clone for ApiRouter { fn clone(&self) -> Self { Self { paths: self.paths.clone(), @@ -220,41 +221,34 @@ impl Clone for ApiRouter { } } -impl Service> for ApiRouter<(), B> -where - B: HttpBody + Send + 'static, -{ +impl Service> for ApiRouter<()> { type Response = axum::response::Response; type Error = Infallible; - type Future = axum::routing::future::RouteFuture; + type Future = axum::routing::future::RouteFuture; #[inline] fn poll_ready( &mut self, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - self.router.poll_ready(cx) + >>::poll_ready(&mut self.router, cx) } #[inline] - fn call(&mut self, req: Request) -> Self::Future { + fn call(&mut self, req: Request) -> Self::Future { self.router.call(req) } } #[allow(clippy::mismatching_type_param_order)] -impl Default for ApiRouter<(), B> -where - B: HttpBody + Send + 'static, -{ +impl Default for ApiRouter<()> { fn default() -> Self { Self::new() } } -impl ApiRouter +impl ApiRouter where - B: HttpBody + Send + 'static, S: Clone + Send + Sync + 'static, { /// Create a new router. @@ -270,7 +264,7 @@ where /// Add state to the router. /// /// See [`axum::Router::with_state`] for details. - pub fn with_state(self, state: S) -> ApiRouter { + pub fn with_state(self, state: S) -> ApiRouter { ApiRouter { paths: self.paths, router: self.router.with_state(state), @@ -297,7 +291,7 @@ where /// /// See [`axum::Router::route`] for details. #[tracing::instrument(skip_all, fields(%path))] - pub fn api_route(mut self, path: &str, mut method_router: ApiMethodRouter) -> Self { + pub fn api_route(mut self, path: &str, mut method_router: ApiMethodRouter) -> Self { in_context(|ctx| { let new_path_item = method_router.take_path_item(); @@ -323,7 +317,7 @@ where pub fn api_route_with( mut self, path: &str, - mut method_router: ApiMethodRouter, + mut method_router: ApiMethodRouter, transform: impl FnOnce(TransformPathItem) -> TransformPathItem, ) -> Self { in_context(|ctx| { @@ -346,7 +340,7 @@ where /// Turn this router into an [`axum::Router`] while merging /// generated documentation into the provided [`OpenApi`]. #[tracing::instrument(skip_all)] - pub fn finish_api(mut self, api: &mut OpenApi) -> Router { + pub fn finish_api(mut self, api: &mut OpenApi) -> Router { self.merge_api(api); self.router } @@ -357,7 +351,7 @@ where /// This method accepts a transform function to edit /// the generated API documentation with. #[tracing::instrument(skip_all)] - pub fn finish_api_with(mut self, api: &mut OpenApi, transform: F) -> Router + pub fn finish_api_with(mut self, api: &mut OpenApi, transform: F) -> Router where F: FnOnce(TransformOpenApi) -> TransformOpenApi, { @@ -420,16 +414,15 @@ where } /// Existing methods extended with api-specifics. -impl ApiRouter +impl ApiRouter where - B: HttpBody + Send + 'static, S: Clone + Send + Sync + 'static, { /// See [`axum::Router::route`] for details. /// /// This method accepts [`ApiMethodRouter`] but does not generate API documentation. #[tracing::instrument(skip_all)] - pub fn route(mut self, path: &str, method_router: impl Into>) -> Self { + pub fn route(mut self, path: &str, method_router: impl Into>) -> Self { self.router = self.router.route(path, method_router.into().router); self } @@ -438,7 +431,7 @@ where #[tracing::instrument(skip_all)] pub fn route_service(mut self, path: &str, service: T) -> Self where - T: Service, Error = Infallible> + Clone + Send + 'static, + T: Service, Error = Infallible> + Clone + Send + 'static, T::Response: IntoResponse, T::Future: Send + 'static, { @@ -450,7 +443,7 @@ where /// /// The generated documentations are nested as well. #[tracing::instrument(skip_all)] - pub fn nest(mut self, mut path: &str, router: ApiRouter) -> Self { + pub fn nest(mut self, mut path: &str, router: ApiRouter) -> Self { self.router = self.router.nest(path, router.router); path = path.trim_end_matches('/'); @@ -474,12 +467,8 @@ where /// /// Thus the primary and probably the only use-case /// of this function is nesting routers with different states. - pub fn nest_api_service( - mut self, - mut path: &str, - service: impl Into>, - ) -> Self { - let router: ApiRouter<(), B> = service.into(); + pub fn nest_api_service(mut self, mut path: &str, service: impl Into>) -> Self { + let router: ApiRouter<()> = service.into(); path = path.trim_end_matches('/'); self.paths.extend( @@ -496,7 +485,7 @@ where /// to pass on the API documentation from the nested service as well. pub fn nest_service(mut self, path: &str, svc: T) -> Self where - T: Service, Error = Infallible> + Clone + Send + 'static, + T: Service, Error = Infallible> + Clone + Send + 'static, T::Response: IntoResponse, T::Future: Send + 'static, { @@ -511,9 +500,9 @@ where /// are merged as well.. pub fn merge(mut self, other: R) -> Self where - R: Into>, + R: Into>, { - let other: ApiRouter = other.into(); + let other: ApiRouter = other.into(); for (key, path) in other.paths { match self.paths.entry(key) { @@ -530,14 +519,13 @@ where } /// See [`axum::Router::layer`] for details. - pub fn layer(self, layer: L) -> ApiRouter + pub fn layer(self, layer: L) -> ApiRouter where - L: Layer> + Clone + Send + 'static, - L::Service: Service> + Clone + Send + 'static, - >>::Response: IntoResponse + 'static, - >>::Error: Into + 'static, - >>::Future: Send + 'static, - NewReqBody: HttpBody + 'static, + L: Layer + Clone + Send + 'static, + L::Service: Service> + Clone + Send + 'static, + >>::Response: IntoResponse + 'static, + >>::Error: Into + 'static, + >>::Future: Send + 'static, { ApiRouter { paths: self.paths, @@ -548,11 +536,11 @@ where /// See [`axum::Router::route_layer`] for details. pub fn route_layer(mut self, layer: L) -> Self where - L: Layer> + Clone + Send + 'static, - L::Service: Service> + Clone + Send + 'static, - >>::Response: IntoResponse + 'static, - >>::Error: Into + 'static, - >>::Future: Send + 'static, + L: Layer + Clone + Send + 'static, + L::Service: Service> + Clone + Send + 'static, + >>::Response: IntoResponse + 'static, + >>::Error: Into + 'static, + >>::Future: Send + 'static, { self.router = self.router.route_layer(layer); self @@ -561,7 +549,7 @@ where /// See [`axum::Router::fallback`] for details. pub fn fallback(mut self, handler: H) -> Self where - H: Handler, + H: Handler, T: 'static, { self.router = self.router.fallback(handler); @@ -571,7 +559,7 @@ where /// See [`axum::Router::fallback_service`] for details. pub fn fallback_service(mut self, svc: T) -> Self where - T: Service, Error = Infallible> + Clone + Send + 'static, + T: Service, Error = Infallible> + Clone + Send + 'static, T::Response: IntoResponse, T::Future: Send + 'static, { @@ -580,14 +568,11 @@ where } } -impl ApiRouter<(), B> -where - B: HttpBody + Send + 'static, -{ +impl ApiRouter<()> { /// See [`axum::Router::into_make_service`] for details. #[tracing::instrument(skip_all)] #[must_use] - pub fn into_make_service(self) -> IntoMakeService> { + pub fn into_make_service(self) -> IntoMakeService> { self.router.into_make_service() } @@ -596,13 +581,13 @@ where #[must_use] pub fn into_make_service_with_connect_info( self, - ) -> IntoMakeServiceWithConnectInfo, C> { + ) -> IntoMakeServiceWithConnectInfo, C> { self.router.into_make_service_with_connect_info() } } -impl From> for ApiRouter { - fn from(router: Router) -> Self { +impl From> for ApiRouter { + fn from(router: Router) -> Self { ApiRouter { paths: IndexMap::new(), router, @@ -610,8 +595,8 @@ impl From> for ApiRouter { } } -impl From> for Router { - fn from(api: ApiRouter) -> Self { +impl From> for Router { + fn from(api: ApiRouter) -> Self { api.router } } @@ -628,44 +613,43 @@ pub trait IntoApiResponse: IntoResponse + OperationOutput {} impl IntoApiResponse for T where T: IntoResponse + OperationOutput {} /// Convenience extension trait for [`axum::Router`]. -pub trait RouterExt: private::Sealed + Sized { +pub trait RouterExt: private::Sealed + Sized { /// Turn the router into an [`ApiRouter`] to enable /// automatic generation of API documentation. - fn into_api(self) -> ApiRouter; + fn into_api(self) -> ApiRouter; /// Add an API route, see [`ApiRouter::api_route`](crate::axum::ApiRouter::api_route) /// for details. /// /// This method additionally turns the router into an [`ApiRouter`]. - fn api_route(self, path: &str, method_router: ApiMethodRouter) -> ApiRouter; + fn api_route(self, path: &str, method_router: ApiMethodRouter) -> ApiRouter; } -impl RouterExt for Router +impl RouterExt for Router where - B: HttpBody + Send + 'static, S: Clone + Send + Sync + 'static, { #[tracing::instrument(skip_all)] - fn into_api(self) -> ApiRouter { + fn into_api(self) -> ApiRouter { ApiRouter::from(self) } #[tracing::instrument(skip_all)] - fn api_route(self, path: &str, method_router: ApiMethodRouter) -> ApiRouter { + fn api_route(self, path: &str, method_router: ApiMethodRouter) -> ApiRouter { ApiRouter::from(self).api_route(path, method_router) } } -impl private::Sealed for Router {} +impl private::Sealed for Router {} #[doc(hidden)] -pub enum ServiceOrApiRouter { +pub enum ServiceOrApiRouter { Service(T), - Router(ApiRouter<(), B>), + Router(ApiRouter<()>), } -impl From for ServiceOrApiRouter +impl From for ServiceOrApiRouter where - T: Service, Error = Infallible> + Clone + Send + 'static, + T: Service, Error = Infallible> + Clone + Send + 'static, T::Response: IntoResponse, T::Future: Send + 'static, { @@ -674,8 +658,8 @@ where } } -impl From> for ServiceOrApiRouter { - fn from(v: ApiRouter<(), B>) -> Self { +impl From> for ServiceOrApiRouter { + fn from(v: ApiRouter<()>) -> Self { Self::Router(v) } } @@ -685,7 +669,7 @@ impl From> for ServiceOrApiRouter { #[doc(hidden)] pub enum DefinitelyNotService {} -impl Service> for DefinitelyNotService { +impl Service> for DefinitelyNotService { type Response = String; type Error = Infallible; @@ -700,7 +684,7 @@ impl Service> for DefinitelyNotService { unreachable!() } - fn call(&mut self, _req: Request) -> Self::Future { + fn call(&mut self, _req: Request) -> Self::Future { unreachable!() } } @@ -714,16 +698,16 @@ mod private { /// /// Just like axum's `Handler`, it is automatically implemented /// for the appropriate types. -pub trait AxumOperationHandler: Handler + OperationHandler +pub trait AxumOperationHandler: Handler + OperationHandler where I: OperationInput, O: OperationOutput, { } -impl AxumOperationHandler for H +impl AxumOperationHandler for H where - H: Handler + OperationHandler, + H: Handler + OperationHandler, I: OperationInput, O: OperationOutput, { diff --git a/crates/aide/src/axum/routing.rs b/crates/aide/src/axum/routing.rs index 347ba1f..9a9e1b4 100644 --- a/crates/aide/src/axum/routing.rs +++ b/crates/aide/src/axum/routing.rs @@ -8,15 +8,12 @@ use crate::{ openapi::{Operation, PathItem, ReferenceOr, Response, StatusCode}, Error, }; -use axum::body::HttpBody; +use axum::body::{Body, HttpBody}; use axum::routing::Route; use axum::{ - body::Body, handler::Handler, routing::{self, MethodRouter}, - BoxError, }; -use bytes::Bytes; use http::Request; use indexmap::IndexMap; use tower_layer::Layer; @@ -31,19 +28,19 @@ use crate::{ /// A wrapper over [`axum::routing::MethodRouter`] that adds /// API documentation-specific features. #[must_use] -pub struct ApiMethodRouter { +pub struct ApiMethodRouter { pub(crate) operations: IndexMap<&'static str, Operation>, - pub(crate) router: MethodRouter, + pub(crate) router: MethodRouter, } -impl From> for MethodRouter { - fn from(router: ApiMethodRouter) -> Self { +impl From> for MethodRouter { + fn from(router: ApiMethodRouter) -> Self { router.router } } -impl From> for ApiMethodRouter { - fn from(router: MethodRouter) -> Self { +impl From> for ApiMethodRouter { + fn from(router: MethodRouter) -> Self { Self { operations: IndexMap::default(), router, @@ -51,7 +48,7 @@ impl From> for ApiMethodRouter { } } -impl ApiMethodRouter { +impl ApiMethodRouter { pub(crate) fn take_path_item(&mut self) -> PathItem { let mut path = PathItem::default(); @@ -76,25 +73,14 @@ impl ApiMethodRouter { macro_rules! method_router_chain_method { ($name:ident, $name_with:ident) => { #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::MethodRouter::", stringify!($name) , "`] for more details.")] - pub fn $name(mut self, handler: H) -> Self + pub fn $name(self, handler: H) -> Self where - H: Handler + OperationHandler, + H: Handler + OperationHandler, I: OperationInput, O: OperationOutput, - B: Send + 'static, T: 'static, { - let mut operation = Operation::default(); - in_context(|ctx| { - I::operation_input(ctx, &mut operation); - - for (code, res) in O::inferred_responses(ctx, &mut operation) { - set_inferred_response(ctx, &mut operation, code, res); - } - }); - self.operations.insert(stringify!($name), operation); - self.router = self.router.$name(handler); - self + self.$name_with(handler, |t| t) } #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::MethodRouter::", stringify!($name) , "`] for more details.")] @@ -103,10 +89,9 @@ macro_rules! method_router_chain_method { /// see [`crate::axum`] for more details. pub fn $name_with(mut self, handler: H, transform: F) -> Self where - H: Handler + OperationHandler, + H: Handler + OperationHandler, I: OperationInput, O: OperationOutput, - B: Send + 'static, T: 'static, F: FnOnce(TransformOperation) -> TransformOperation, { @@ -145,36 +130,15 @@ macro_rules! method_router_top_level { ($name:ident, $name_with:ident) => { #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::", stringify!($name) , "`] for more details.")] #[tracing::instrument(skip_all)] - pub fn $name(handler: H) -> ApiMethodRouter + pub fn $name(handler: H) -> ApiMethodRouter where - H: Handler + OperationHandler, + H: Handler + OperationHandler, I: OperationInput, O: OperationOutput, - B: HttpBody + Send + Sync + 'static, S: Clone + Send + Sync + 'static, T: 'static, { - let mut router = ApiMethodRouter::from(routing::$name(handler)); - let mut operation = Operation::default(); - in_context(|ctx| { - I::operation_input(ctx, &mut operation); - - for (code, res) in O::inferred_responses(ctx, &mut operation) { - set_inferred_response(ctx, &mut operation, code, res); - } - - // On conflict, input early responses potentially overwrite - // output inferred responses on purpose, as they - // are stronger in a sense that the request won't - // even reach the handler body. - for (code, res) in I::inferred_early_responses(ctx, &mut operation) { - set_inferred_response(ctx, &mut operation, code, res); - } - }); - - router.operations.insert(stringify!($name), operation); - - router + $crate::axum::routing::$name_with(handler, |t| t) } #[doc = concat!("Route `", stringify!($name) ,"` requests to the given handler. See [`axum::routing::", stringify!($name) , "`] for more details.")] @@ -182,15 +146,14 @@ macro_rules! method_router_top_level { /// This method additionally accepts a transform function, /// see [`crate::axum`] for more details. #[tracing::instrument(skip_all)] - pub fn $name_with( + pub fn $name_with( handler: H, transform: F, - ) -> ApiMethodRouter + ) -> ApiMethodRouter where - H: Handler + OperationHandler, + H: Handler + OperationHandler, I: OperationInput, O: OperationOutput, - B: axum::body::HttpBody + Send + Sync + 'static, S: Clone + Send + Sync + 'static, T: 'static, F: FnOnce(TransformOperation) -> TransformOperation, @@ -258,10 +221,9 @@ fn set_inferred_response( } } -impl ApiMethodRouter +impl ApiMethodRouter where S: Clone + Send + Sync + 'static, - B: HttpBody + Send + Sync + 'static, { method_router_chain_method!(delete, delete_with); method_router_chain_method!(get, get_with); @@ -274,25 +236,16 @@ where /// This method wraps a layer around the [`ApiMethodRouter`] /// For further information see [`axum::routing::method_routing::MethodRouter::layer`] - pub fn layer( - self, - layer: L, - ) -> ApiMethodRouter + pub fn layer(self, layer: L) -> ApiMethodRouter where - L: Layer> + Clone + Send + 'static, - L::Service: Service< - Request, - Response = http::response::Response, - Error = NewError, - > + Clone + L: Layer> + Clone + Send + 'static, + L::Service: Service, Response = http::response::Response, Error = NewError> + + Clone + Send + 'static, - >>::Future: Send + 'static, - NewResBody: 'static, - NewReqBody: HttpBody + 'static, + >>::Future: Send + 'static, + Body: HttpBody + 'static, NewError: 'static, - NewResBody: HttpBody + Send + 'static, - NewResBody::Error: Into, { ApiMethodRouter { router: self.router.layer(layer), @@ -301,22 +254,21 @@ where } } -impl ApiMethodRouter +impl ApiMethodRouter where - B: HttpBody + Send + 'static, S: Clone, { /// Create a new, clean [`ApiMethodRouter`] based on [`MethodRouter::new()`](axum::routing::MethodRouter). pub fn new() -> Self { Self { operations: IndexMap::default(), - router: MethodRouter::::new(), + router: MethodRouter::::new(), } } /// See [`axum::routing::MethodRouter`] and [`axum::extract::State`] for more information. - pub fn with_state(self, state: S) -> ApiMethodRouter { + pub fn with_state(self, state: S) -> ApiMethodRouter { let router = self.router.with_state(state); - ApiMethodRouter:: { + ApiMethodRouter:: { operations: self.operations, router, } @@ -325,7 +277,7 @@ where /// See [`axum::routing::MethodRouter::merge`] for more information. pub fn merge(mut self, other: M) -> Self where - M: Into> + M: Into>, { let other = other.into(); self.operations.extend(other.operations); @@ -334,9 +286,8 @@ where } } -impl Default for ApiMethodRouter +impl Default for ApiMethodRouter where - B: HttpBody + Send + 'static, S: Clone, { fn default() -> Self { diff --git a/crates/aide/src/helpers/no_api.rs b/crates/aide/src/helpers/no_api.rs index ce192f9..1dd934f 100644 --- a/crates/aide/src/helpers/no_api.rs +++ b/crates/aide/src/helpers/no_api.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use crate::{OperationInput, OperationOutput}; -/// Allows non [OperationInput] or [OperationOutput] types to be used in aide handlers with a default empty documentation. -/// For types that already implement [OperationInput] or [OperationOutput] it overrides the documentation and hides it. +/// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with a default empty documentation. +/// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation and hides it. /// ```ignore /// pub async fn my_sqlx_tx_endpoint( /// NoApi(mut tx): NoApi> // allows usage of the TX @@ -45,9 +45,9 @@ impl OperationOutput for NoApi { #[cfg(feature = "axum")] mod axum { - use axum::async_trait; use axum::extract::{FromRequest, FromRequestParts}; use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; + use axum::{async_trait, body::Body}; use http::request::Parts; use http::Request; @@ -87,15 +87,14 @@ mod axum { } #[async_trait] - impl FromRequest for NoApi + impl FromRequest for NoApi where - T: FromRequest, + T: FromRequest, S: Send + Sync, - B: Send + 'static, { type Rejection = ::Rejection; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { Ok(Self(::from_request(req, state).await?)) } } diff --git a/crates/aide/src/helpers/use_api.rs b/crates/aide/src/helpers/use_api.rs index fef6cab..a98c447 100644 --- a/crates/aide/src/helpers/use_api.rs +++ b/crates/aide/src/helpers/use_api.rs @@ -7,34 +7,39 @@ use crate::gen::GenContext; use crate::openapi::{Operation, Response}; use crate::{OperationInput, OperationOutput}; -/// helper trait to allow simplified use of [UseApi] in responses +/// helper trait to allow simplified use of [`UseApi`] in responses pub trait IntoApi { - fn into_api(self) -> UseApi where Self: Sized; + fn into_api(self) -> UseApi + where + Self: Sized; } impl IntoApi for T { - fn into_api(self) -> UseApi where Self: Sized { + fn into_api(self) -> UseApi + where + Self: Sized, + { self.into() } } -/// Allows non [OperationInput] or [OperationOutput] types to be used in aide handlers with the api documentation of [A]. -/// For types that already implement [OperationInput] or [OperationOutput] it overrides the documentation with the provided one. +/// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with the api documentation of [A]. +/// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation with the provided one. #[derive( -Copy, -Clone, -Debug, -Ord, -PartialOrd, -Eq, -PartialEq, -Hash, -Serialize, -Deserialize, -Deref, -DerefMut, -AsRef, -AsMut, + Copy, + Clone, + Debug, + Ord, + PartialOrd, + Eq, + PartialEq, + Hash, + Serialize, + Deserialize, + Deref, + DerefMut, + AsRef, + AsMut, )] pub struct UseApi( #[as_ref] @@ -51,8 +56,7 @@ impl From for UseApi { } } -impl UseApi -{ +impl UseApi { /// Unwraps [Self] into its inner type pub fn into_inner(self) -> T { self.0 @@ -60,11 +64,11 @@ impl UseApi } impl OperationInput for UseApi - where - A: OperationInput, +where + A: OperationInput, { fn operation_input(ctx: &mut GenContext, operation: &mut Operation) { - A::operation_input(ctx, operation) + A::operation_input(ctx, operation); } fn inferred_early_responses( @@ -76,8 +80,8 @@ impl OperationInput for UseApi } impl OperationOutput for UseApi - where - A: OperationOutput, +where + A: OperationOutput, { type Inner = A::Inner; @@ -95,17 +99,17 @@ impl OperationOutput for UseApi #[cfg(feature = "axum")] mod axum { - use axum::async_trait; use axum::extract::{FromRequest, FromRequestParts}; use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; + use axum::{async_trait, body::Body}; use http::request::Parts; use http::Request; use crate::UseApi; impl IntoResponse for UseApi - where - T: IntoResponse, + where + T: IntoResponse, { fn into_response(self) -> Response { self.0.into_response() @@ -113,8 +117,8 @@ mod axum { } impl IntoResponseParts for UseApi - where - T: IntoResponseParts, + where + T: IntoResponseParts, { type Error = T::Error; @@ -125,9 +129,9 @@ mod axum { #[async_trait] impl FromRequestParts for UseApi - where - T: FromRequestParts, - S: Send + Sync, + where + T: FromRequestParts, + S: Send + Sync, { type Rejection = T::Rejection; @@ -140,19 +144,15 @@ mod axum { } #[async_trait] - impl FromRequest for UseApi - where - T: FromRequest, - S: Send + Sync, - B: Send + 'static, + impl FromRequest for UseApi + where + T: FromRequest, + S: Send + Sync, { type Rejection = T::Rejection; - async fn from_request(req: Request, state: &S) -> Result { - Ok(Self( - T::from_request(req, state).await?, - Default::default(), - )) + async fn from_request(req: Request, state: &S) -> Result { + Ok(Self(T::from_request(req, state).await?, Default::default())) } } } diff --git a/crates/aide/src/helpers/with_api.rs b/crates/aide/src/helpers/with_api.rs index a064b2a..6d269b2 100644 --- a/crates/aide/src/helpers/with_api.rs +++ b/crates/aide/src/helpers/with_api.rs @@ -62,9 +62,9 @@ pub trait ApiOverride { type Target; } -/// Allows non [OperationInput] or [OperationOutput] types to be used in aide handlers with a provided documentation. -/// For types that already implement [OperationInput] or [OperationOutput] it overrides the documentation with the provided one. -/// See [ApiOverride] on how to implement such an override +/// Allows non [`OperationInput`] or [`OperationOutput`] types to be used in aide handlers with a provided documentation. +/// For types that already implement [`OperationInput`] or [`OperationOutput`] it overrides the documentation with the provided one. +/// See [`ApiOverride`] on how to implement such an override #[derive( Copy, Clone, @@ -107,7 +107,7 @@ where T: OperationInput + ApiOverride, { fn operation_input(ctx: &mut GenContext, operation: &mut Operation) { - T::operation_input(ctx, operation) + T::operation_input(ctx, operation); } fn inferred_early_responses( @@ -139,9 +139,9 @@ where #[cfg(feature = "axum")] mod axum { use crate::helpers::with_api::ApiOverride; - use axum::async_trait; use axum::extract::{FromRequest, FromRequestParts}; use axum::response::{IntoResponse, IntoResponseParts, Response, ResponseParts}; + use axum::{async_trait, body::Body}; use http::request::Parts; use http::Request; @@ -187,18 +187,17 @@ mod axum { } #[async_trait] - impl FromRequest for WithApi + impl FromRequest for WithApi where T: ApiOverride, - T::Target: FromRequest, + T::Target: FromRequest, S: Send + Sync, - B: Send + 'static, { - type Rejection = >::Rejection; + type Rejection = >::Rejection; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { Ok(Self( - >::from_request(req, state).await?, + >::from_request(req, state).await?, Default::default(), )) } diff --git a/crates/aide/src/redoc/mod.rs b/crates/aide/src/redoc/mod.rs index 1abca71..0ca2613 100644 --- a/crates/aide/src/redoc/mod.rs +++ b/crates/aide/src/redoc/mod.rs @@ -52,18 +52,20 @@ //! }, //! ..OpenApi::default() //! }; +//! +//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); //! -//! axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) -//! .serve( -//! app -//! // Generate the documentation. -//! .finish_api(&mut api) -//! // Expose the documentation to the handlers. -//! .layer(Extension(api)) -//! .into_make_service(), -//! ) -//! .await -//! .unwrap(); +//! axum::serve( +//! listener, +//! app +//! // Generate the documentation. +//! .finish_api(&mut api) +//! // Expose the documentation to the handlers. +//! .layer(Extension(api)) +//! .into_make_service(), +//! ) +//! .await +//! .unwrap(); //! } //! ``` @@ -162,12 +164,9 @@ mod axum_impl { /// ); /// ``` #[must_use] - pub fn axum_handler( + pub fn axum_handler( &self, - ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S, B> - where - B: axum::body::HttpBody + Send + 'static, - { + ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S> { let html = self.html(); // This string will be used during the entire lifetime of the program // so it's safe to leak it diff --git a/crates/axum-jsonschema/Cargo.toml b/crates/axum-jsonschema/Cargo.toml index 4fa7a74..c2f0660 100644 --- a/crates/axum-jsonschema/Cargo.toml +++ b/crates/axum-jsonschema/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "axum-jsonschema" -version = "0.7.0" +version = "0.8.0" edition = "2021" authors = ["tamasfe"] keywords = ["web", "axum", "json"] @@ -10,14 +10,14 @@ description = "Request JSON schema validation for axum" readme = "README.md" [dependencies] -aide = { version = "0.12.0", path = "../aide", optional = true, features = [ +aide = { version = "0.13.0", path = "../aide", optional = true, features = [ "axum", ] } async-trait = "0.1.57" -axum = { version = "0.6.0", default-features = false, features = ["json"] } -http = "0.2.8" -http-body = "0.4.5" -itertools = "0.10.5" +axum = { version = "0.7.1", default-features = false, features = ["json"] } +http = "1.0.0" +http-body = "1.0.0" +itertools = "0.12.0" jsonschema = { version = "0.17.0", default-features = false } schemars = { version = "0.8.10", default-features = false } serde = { version = "1.0.144", features = ["derive"] } diff --git a/crates/axum-jsonschema/src/lib.rs b/crates/axum-jsonschema/src/lib.rs index 0f89a03..917cd1f 100644 --- a/crates/axum-jsonschema/src/lib.rs +++ b/crates/axum-jsonschema/src/lib.rs @@ -23,9 +23,9 @@ use std::{ use async_trait::async_trait; use axum::{ + body::Body, extract::{rejection::JsonRejection, FromRequest}, response::IntoResponse, - BoxError, }; use http::{Request, StatusCode}; use itertools::Itertools; @@ -47,18 +47,15 @@ use serde_path_to_error::Segment; pub struct Json(pub T); #[async_trait] -impl FromRequest for Json +impl FromRequest for Json where - B: http_body::Body + Send + 'static, - B::Data: Send, - B::Error: Into, S: Send + Sync, T: DeserializeOwned + JsonSchema + 'static, { type Rejection = JsonSchemaRejection; /// Perform the extraction. - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request(req: Request, state: &S) -> Result { let value: Value = match axum::Json::from_request(req, state).await { Ok(j) => j.0, Err(error) => { diff --git a/examples/example-axum/Cargo.toml b/examples/example-axum/Cargo.toml index 9c2e627..8c63e5c 100644 --- a/examples/example-axum/Cargo.toml +++ b/examples/example-axum/Cargo.toml @@ -12,12 +12,12 @@ aide = { path = "../../crates/aide", features = [ "macros", ] } async-trait = "0.1.57" -axum = { version = "0.6.0", features = ["macros"] } -axum-extra = "0.7.4" +axum = { version = "0.7.1", features = ["macros"] } +axum-extra = "0.9.0" axum-jsonschema = { path = "../../crates/axum-jsonschema", features = [ "aide", ] } -axum-macros = "0.3.0" +axum-macros = "0.4.0" schemars = { version = "0.8.10", features = ["uuid1"] } serde = { version = "1.0.144", features = ["derive", "rc"] } serde_json = "1.0.85" diff --git a/examples/example-axum/src/main.rs b/examples/example-axum/src/main.rs index dcfb39c..d85d0a8 100644 --- a/examples/example-axum/src/main.rs +++ b/examples/example-axum/src/main.rs @@ -11,6 +11,7 @@ use errors::AppError; use extractors::Json; use state::AppState; use todos::routes::todo_routes; +use tokio::net::TcpListener; use uuid::Uuid; pub mod docs; @@ -40,10 +41,9 @@ async fn main() { println!("Example docs are accessible at http://127.0.0.1:3000/docs"); - axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) - .serve(app.into_make_service()) - .await - .unwrap(); + let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap(); + + axum::serve(listener, app).await.unwrap(); } fn api_docs(api: TransformOpenApi) -> TransformOpenApi { From 808a48bc524b47e0cbf20264dd8c62f16554f2f9 Mon Sep 17 00:00:00 2001 From: tamasfe Date: Tue, 28 Nov 2023 14:20:37 +0100 Subject: [PATCH 2/2] feat(aide): BREAKING, infer responses and disable schema inlining by default --- crates/aide/src/gen.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/aide/src/gen.rs b/crates/aide/src/gen.rs index ebcd85a..c4fe5c7 100644 --- a/crates/aide/src/gen.rs +++ b/crates/aide/src/gen.rs @@ -42,14 +42,11 @@ pub fn on_error(handler: impl Fn(Error) + 'static) { /// Collect common schemas in the thread-local context, /// then store them under `#/components/schemas` the next /// time generated content is merged into [`OpenApi`]. -/// This feature is disabled by default. +/// This feature is enabled by default. /// /// This will automatically clear the schemas stored /// in the context when they are merged into the documentation. /// -/// **warning**: This might cause name conflicts that are not detected! -/// For more information see . -/// /// [`OpenApi`]: crate::openapi::OpenApi pub fn extract_schemas(extract: bool) { in_context(|ctx| { @@ -83,15 +80,17 @@ pub fn inferred_empty_response_status(status: u16) { /// Infer responses based on request handler /// return types. /// -/// This is disabled by default to avoid incorrect -/// documentation. +/// This is enabled by default. pub fn infer_responses(infer: bool) { in_context(|ctx| { ctx.infer_responses = infer; }); } -/// Output All error responses based on axum. +/// Output all theoretically possbile error responses +/// including framework-specific ones. +/// +/// This is disabled by default. pub fn all_error_responses(infer: bool) { in_context(|ctx| { ctx.all_error_responses = infer; @@ -146,11 +145,11 @@ impl GenContext { Self { schema: SchemaGenerator::new( - SchemaSettings::draft07().with(|s| s.inline_subschemas = true), + SchemaSettings::draft07().with(|s| s.inline_subschemas = false), ), - infer_responses: false, + infer_responses: true, all_error_responses: false, - extract_schemas: false, + extract_schemas: true, show_error: default_error_filter, error_handler: None, no_content_status,