-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Wip initial implementation of service config
Implements `ServiceConfig` for actix-web with scope support. This allows users to register `services` directly to OpenApi via `utiopa` App and custom service config without the need to register paths via `#[openapi(paths(...))]`. Closes #121
- Loading branch information
Showing
6 changed files
with
461 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[package] | ||
name = "utoipa-actix-web" | ||
version = "0.1.0" | ||
edition = "2021" | ||
rust-version.workspace = true | ||
|
||
[dependencies] | ||
utoipa = { path = "../utoipa", version = "5" } | ||
actix-web = { version = "4", default-features = false } | ||
actix-service = "2" | ||
once_cell = "1" | ||
|
||
[dev-dependencies] | ||
utoipa = { path = "../utoipa", version = "5", features = [ | ||
"actix_extras", | ||
"macros", | ||
"debug", | ||
] } | ||
actix-web = { version = "4", default-features = false, features = ["macros"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
use core::fmt; | ||
use std::future::Future; | ||
use std::rc::Rc; | ||
|
||
use actix_service::{IntoServiceFactory, ServiceFactory}; | ||
use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse}; | ||
use actix_web::Error; | ||
use utoipa::openapi::PathItem; | ||
use utoipa::OpenApi; | ||
|
||
use self::service_config::ServiceConfig; | ||
|
||
pub mod scope; | ||
pub mod service_config; | ||
|
||
pub trait PathsFactory { | ||
fn paths(&self) -> utoipa::openapi::path::Paths; | ||
} | ||
|
||
impl<T: utoipa::Path> PathsFactory for T { | ||
fn paths(&self) -> utoipa::openapi::path::Paths { | ||
let methods = T::methods(); | ||
|
||
methods | ||
.into_iter() | ||
.fold( | ||
utoipa::openapi::path::Paths::builder(), | ||
|mut builder, method| { | ||
builder = builder.path(T::path(), PathItem::new(method, T::operation())); | ||
|
||
builder | ||
}, | ||
) | ||
.build() | ||
} | ||
} | ||
|
||
pub trait AppExt<T> { | ||
fn into_utoipa_app(self) -> UtoipaApp<T>; | ||
} | ||
|
||
impl<T> AppExt<T> for actix_web::App<T> { | ||
fn into_utoipa_app(self) -> UtoipaApp<T> { | ||
UtoipaApp::from(self) | ||
} | ||
} | ||
|
||
pub struct UtoipaApp<T>(actix_web::App<T>, Rc<utoipa::openapi::OpenApi>); | ||
|
||
impl<T> From<actix_web::App<T>> for UtoipaApp<T> { | ||
fn from(value: actix_web::App<T>) -> Self { | ||
#[derive(OpenApi)] | ||
struct Api; | ||
UtoipaApp(value, Rc::new(Api::openapi())) | ||
} | ||
} | ||
|
||
impl<T> UtoipaApp<T> | ||
where | ||
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>, | ||
{ | ||
/// Passthrough implementation for [`actix_web::App::app_data`]. | ||
pub fn app_data<U: 'static>(self, data: U) -> Self { | ||
let app = self.0.app_data(data); | ||
Self(app, self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::data_factory`]. | ||
pub fn data_factory<F, Out, D, E>(self, data: F) -> Self | ||
where | ||
F: Fn() -> Out + 'static, | ||
Out: Future<Output = Result<D, E>> + 'static, | ||
D: 'static, | ||
E: std::fmt::Debug, | ||
{ | ||
let app = self.0.data_factory(data); | ||
|
||
Self(app, self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::configure`]. | ||
pub fn configure<F>(mut self, f: F) -> Self | ||
where | ||
F: FnOnce(&mut ServiceConfig), | ||
{ | ||
// TODO get OpenAPI paths???? | ||
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect( | ||
"OpenApi should not have more than one reference when building App with `configure`", | ||
); | ||
|
||
let app = self.0.configure(|config| { | ||
let mut service_config = ServiceConfig::new(config); | ||
|
||
f(&mut service_config); | ||
|
||
let ServiceConfig(_, paths) = service_config; | ||
api.paths.paths.extend(paths.take().paths); | ||
}); | ||
|
||
Self(app, self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::route`]. | ||
pub fn route(self, path: &str, route: actix_web::Route) -> Self { | ||
let app = self.0.route(path, route); | ||
|
||
Self(app, self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::service`]. | ||
pub fn service<F>(mut self, factory: F) -> Self | ||
where | ||
F: HttpServiceFactory + PathsFactory + 'static, | ||
{ | ||
let paths = factory.paths(); | ||
|
||
// TODO should this be `make_mut`? | ||
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect( | ||
"OpenApi should not have more than one reference when building App with `service`", | ||
); | ||
|
||
api.paths.paths.extend(paths.paths); | ||
let app = self.0.service(factory); | ||
|
||
Self(app, self.1) | ||
} | ||
|
||
pub fn openapi_service<O, F>(self, factory: F) -> Self | ||
where | ||
F: FnOnce(Rc<utoipa::openapi::OpenApi>) -> O, | ||
O: HttpServiceFactory + 'static, | ||
{ | ||
let service = factory(self.1.clone()); | ||
let app = self.0.service(service); | ||
Self(app, self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::default_service`]. | ||
pub fn default_service<F, U>(self, svc: F) -> Self | ||
where | ||
F: IntoServiceFactory<U, ServiceRequest>, | ||
U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error> | ||
+ 'static, | ||
U::InitError: fmt::Debug, | ||
{ | ||
Self(self.0.default_service(svc), self.1) | ||
} | ||
|
||
/// Passthrough implementation for [`actix_web::App::external_resource`]. | ||
pub fn external_resource<N, U>(self, name: N, url: U) -> Self | ||
where | ||
N: AsRef<str>, | ||
U: AsRef<str>, | ||
{ | ||
Self(self.0.external_resource(name, url), self.1) | ||
} | ||
|
||
pub fn map< | ||
F: FnOnce(actix_web::App<T>) -> actix_web::App<NF>, | ||
NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>, | ||
>( | ||
self, | ||
op: F, | ||
) -> UtoipaApp<NF> { | ||
let app = op(self.0); | ||
UtoipaApp(app, self.1) | ||
} | ||
|
||
pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) { | ||
( | ||
self.0, | ||
Rc::try_unwrap(self.1).unwrap_or_else(|rc| (*rc).clone()), | ||
) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use actix_service::Service; | ||
use actix_web::http::header::{HeaderValue, CONTENT_TYPE}; | ||
use actix_web::{get, App}; | ||
|
||
use super::*; | ||
|
||
#[utoipa::path(impl_for = handler2)] | ||
#[get("/handler2")] | ||
async fn handler2() -> &'static str { | ||
"this is message 2" | ||
} | ||
|
||
#[utoipa::path(impl_for = handler)] | ||
#[get("/handler")] | ||
async fn handler() -> &'static str { | ||
"this is message" | ||
} | ||
|
||
#[utoipa::path(impl_for = handler3)] | ||
#[get("/handler3")] | ||
async fn handler3() -> &'static str { | ||
"this is message 3" | ||
} | ||
|
||
#[test] | ||
fn test_app() { | ||
fn config(cfg: &mut service_config::ServiceConfig) { | ||
cfg.service(handler3); | ||
} | ||
|
||
let (_, api) = App::new() | ||
.into_utoipa_app() | ||
.service(handler) | ||
.configure(config) | ||
.service(scope::scope("/path-prefix").service(handler2).map(|scope| { | ||
let s = scope.wrap_fn(|req, srv| { | ||
let fut = srv.call(req); | ||
async { | ||
let mut res = fut.await?; | ||
res.headers_mut() | ||
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); | ||
Ok(res) | ||
} | ||
}); | ||
|
||
s | ||
})) | ||
.split_for_parts(); | ||
|
||
dbg!(api); | ||
// let app = app.service(scope::scope("prefix").service(handler)); | ||
|
||
// app | ||
} | ||
} |
Oops, something went wrong.