diff --git a/Cargo.toml b/Cargo.toml index 5120e29c..2e8fd8d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,6 @@ members = [ "axum-core", "axum-extra", "axum-macros", - "examples/*", + "axum-openapi", + # "examples/*", ] diff --git a/axum-openapi/Cargo.toml b/axum-openapi/Cargo.toml new file mode 100644 index 00000000..732a19c6 --- /dev/null +++ b/axum-openapi/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "axum-openapi" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = { path = "../axum", version = "0.4" } +okapi = "0.7.0-rc.1" +schemars = "0.8.8" diff --git a/axum-openapi/src/lib.rs b/axum-openapi/src/lib.rs new file mode 100644 index 00000000..ce14c277 --- /dev/null +++ b/axum-openapi/src/lib.rs @@ -0,0 +1,156 @@ +#![allow(missing_debug_implementations)] + +use axum::{ + async_trait, + body::HttpBody, + handler::Handler, + http::Request, + response::{IntoResponse, Response}, + routing::{get, post}, + Router, +}; +use okapi::openapi3::{Info, OpenApi, Operation}; +use std::{future::Future, marker::PhantomData, sync::Arc}; + +pub struct OpenApiRouter { + router: Router, + schema: OpenApi, +} + +impl OpenApiRouter +where + B: HttpBody + Send + 'static, +{ + pub fn new(info: Info) -> Self { + Self { + router: Default::default(), + schema: OpenApi { + info, + ..Default::default() + }, + } + } + + pub fn get(mut self, path: &str, mut handler: H) -> Self + where + H: OpenApiHandler, + T: 'static, + { + let mut operation = Operation::default(); + handler.clone().to_operation(&mut operation); + self.schema.paths.entry(path.to_owned()).or_default().get = Some(operation); + self.router = self.router.route(path, get(handler)); + self + } + + pub fn post(mut self, path: &str, mut handler: H) -> Self + where + H: OpenApiHandler, + T: 'static, + { + let mut operation = Operation::default(); + handler.clone().to_operation(&mut operation); + self.schema.paths.entry(path.to_owned()).or_default().post = Some(operation); + self.router = self.router.route(path, post(handler)); + self + } +} + +pub trait OpenApiHandler: Handler { + fn to_operation(self, operation: &mut Operation); + + fn map_operation(self, f: F) -> MapOperation + where + F: FnOnce(&mut Operation), + { + MapOperation { + handler: self, + f, + _marker: PhantomData, + } + } +} + +pub type SetOperationField = MapOperation>; + +pub struct MapOperation { + handler: H, + f: F, + _marker: PhantomData<(T, B)>, +} + +impl Clone for MapOperation +where + H: Clone, + F: Clone, +{ + fn clone(&self) -> Self { + Self { + handler: self.handler.clone(), + f: self.f.clone(), + _marker: PhantomData, + } + } +} + +impl OpenApiHandler for MapOperation +where + H: OpenApiHandler + Handler, + F: FnOnce(&mut Operation) + Clone + Send + 'static, + T: Send + 'static, + B: Send + 'static, +{ + fn to_operation(self, operation: &mut Operation) { + (self.f)(operation); + } +} + +impl Handler for MapOperation +where + H: Handler, + F: Clone + Send + 'static, + T: Send + 'static, + B: Send + 'static, +{ + type Future = H::Future; + + fn call(self, req: Request) -> Self::Future { + self.handler.call(req) + } +} + +impl OpenApiHandler<(), B> for F +where + F: FnOnce() -> Fut + Clone + Send + 'static, + Fut: Future + Send, + Res: IntoResponse, + B: Send + 'static, +{ + fn to_operation(self, operation: &mut Operation) { + todo!() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + + #[test] + fn test_something() { + async fn users_show() {} + + async fn users_create() {} + + let _router: OpenApiRouter = OpenApiRouter::new(Info::default()) + .post( + "/users", + users_create + .map_operation(|_| {}) + .map_operation(|mut operation| { + operation.summary = Some("Create a new user".to_owned()); + }), + ) + .get("/users/:id", users_show.map_operation(|_operation| {})); + } +} diff --git a/axum/src/handler/into_service.rs b/axum/src/handler/into_service.rs index 1cacfa20..bb99d57f 100644 --- a/axum/src/handler/into_service.rs +++ b/axum/src/handler/into_service.rs @@ -71,10 +71,11 @@ where } fn call(&mut self, req: Request) -> Self::Future { - use futures_util::future::FutureExt; + use futures_util::future::{BoxFuture, FutureExt}; let handler = self.handler.clone(); - let future = Handler::call(handler, req).map(Ok::<_, Infallible> as _); + let future = Box::pin(Handler::call(handler, req)) as BoxFuture<'static, _>; + let future = future.map(Ok::<_, Infallible> as _); super::future::IntoServiceFuture::new(future) } diff --git a/axum/src/handler/mod.rs b/axum/src/handler/mod.rs index f30fb775..e39ea8ca 100644 --- a/axum/src/handler/mod.rs +++ b/axum/src/handler/mod.rs @@ -82,7 +82,7 @@ use crate::{ }; use async_trait::async_trait; use http::Request; -use std::{fmt, future::Future, marker::PhantomData}; +use std::{fmt, future::Future, marker::PhantomData, pin::Pin}; use tower::ServiceExt; use tower_layer::Layer; use tower_service::Service; @@ -92,29 +92,17 @@ mod into_service; pub use self::into_service::IntoService; -pub(crate) mod sealed { - #![allow(unreachable_pub, missing_docs, missing_debug_implementations)] - - pub trait HiddenTrait {} - pub struct Hidden; - impl HiddenTrait for Hidden {} -} - /// Trait for async functions that can be used to handle requests. /// /// You shouldn't need to depend on this trait directly. It is automatically /// implemented to closures of the right types. /// /// See the [module docs](crate::handler) for more details. -#[async_trait] pub trait Handler: Clone + Send + Sized + 'static { - // This seals the trait. We cannot use the regular "sealed super trait" - // approach due to coherence. - #[doc(hidden)] - type Sealed: sealed::HiddenTrait; + type Future: Future + Send + 'static; /// Call the handler with the given request. - async fn call(self, req: Request) -> Response; + fn call(self, req: Request) -> Self::Future; /// Apply a [`tower::Layer`] to the handler. /// @@ -260,48 +248,48 @@ pub trait Handler: Clone + Send + Sized + 'static { } } -#[async_trait] -impl Handler<(), B> for F +impl Handler<(), B> for F where F: FnOnce() -> Fut + Clone + Send + 'static, - Fut: Future + Send, - Res: IntoResponse, + Fut: Future + Send, + Fut::Output: IntoResponse, B: Send + 'static, { - type Sealed = sealed::Hidden; + type Future = Pin + Send + 'static>>; - async fn call(self, _req: Request) -> Response { - self().await.into_response() + fn call(self, _req: Request) -> Self::Future { + Box::pin(async move { self().await.into_response() }) } } macro_rules! impl_handler { ( $($ty:ident),* $(,)? ) => { - #[async_trait] #[allow(non_snake_case)] - impl Handler<($($ty,)*), B> for F + impl Handler<($($ty,)*), B> for F where F: FnOnce($($ty,)*) -> Fut + Clone + Send + 'static, - Fut: Future + Send, + Fut: Future + Send, B: Send + 'static, - Res: IntoResponse, + Fut::Output: IntoResponse, $( $ty: FromRequest + Send,)* { - type Sealed = sealed::Hidden; + type Future = Pin + Send + 'static>>; - async fn call(self, req: Request) -> Response { - let mut req = RequestParts::new(req); + fn call(self, req: Request) -> Self::Future { + Box::pin(async move { + let mut req = RequestParts::new(req); - $( - let $ty = match $ty::from_request(&mut req).await { - Ok(value) => value, - Err(rejection) => return rejection.into_response(), - }; - )* + $( + let $ty = match $ty::from_request(&mut req).await { + Ok(value) => value, + Err(rejection) => return rejection.into_response(), + }; + )* - let res = self($($ty,)*).await; + let res = self($($ty,)*).await; - res.into_response() + res.into_response() + }) } } }; @@ -346,13 +334,15 @@ where ResBody: HttpBody + Send + 'static, ResBody::Error: Into, { - type Sealed = sealed::Hidden; + type Future = Pin + Send + 'static>>; - async fn call(self, req: Request) -> Response { - match self.svc.oneshot(req).await { - Ok(res) => res.map(boxed), - Err(res) => res.into_response(), - } + fn call(self, req: Request) -> Self::Future { + Box::pin(async move { + match self.svc.oneshot(req).await { + Ok(res) => res.map(boxed), + Err(res) => res.into_response(), + } + }) } }