Skip to content

Commit 57ccb9f

Browse files
committed
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
1 parent 155657f commit 57ccb9f

File tree

6 files changed

+461
-2
lines changed

6 files changed

+461
-2
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ members = [
1313
"utoipa-scalar",
1414
"utoipa-axum",
1515
"utoipa-config",
16+
"utoipa-actix-web",
1617
]
1718

1819
[workspace.metadata.publish]
@@ -26,4 +27,5 @@ order = [
2627
"utoipa-rapidoc",
2728
"utoipa-scalar",
2829
"utoipa-axum",
30+
"utoipa-actix-web",
2931
]

utoipa-actix-web/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "utoipa-actix-web"
3+
version = "0.1.0"
4+
edition = "2021"
5+
rust-version.workspace = true
6+
7+
[dependencies]
8+
utoipa = { path = "../utoipa", version = "5" }
9+
actix-web = { version = "4", default-features = false }
10+
actix-service = "2"
11+
once_cell = "1"
12+
13+
[dev-dependencies]
14+
utoipa = { path = "../utoipa", version = "5", features = [
15+
"actix_extras",
16+
"macros",
17+
"debug",
18+
] }
19+
actix-web = { version = "4", default-features = false, features = ["macros"] }

utoipa-actix-web/src/lib.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use core::fmt;
2+
use std::future::Future;
3+
use std::rc::Rc;
4+
5+
use actix_service::{IntoServiceFactory, ServiceFactory};
6+
use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse};
7+
use actix_web::Error;
8+
use utoipa::openapi::PathItem;
9+
use utoipa::OpenApi;
10+
11+
use self::service_config::ServiceConfig;
12+
13+
pub mod scope;
14+
pub mod service_config;
15+
16+
pub trait PathsFactory {
17+
fn paths(&self) -> utoipa::openapi::path::Paths;
18+
}
19+
20+
impl<T: utoipa::Path> PathsFactory for T {
21+
fn paths(&self) -> utoipa::openapi::path::Paths {
22+
let methods = T::methods();
23+
24+
methods
25+
.into_iter()
26+
.fold(
27+
utoipa::openapi::path::Paths::builder(),
28+
|mut builder, method| {
29+
builder = builder.path(T::path(), PathItem::new(method, T::operation()));
30+
31+
builder
32+
},
33+
)
34+
.build()
35+
}
36+
}
37+
38+
pub trait AppExt<T> {
39+
fn into_utoipa_app(self) -> UtoipaApp<T>;
40+
}
41+
42+
impl<T> AppExt<T> for actix_web::App<T> {
43+
fn into_utoipa_app(self) -> UtoipaApp<T> {
44+
UtoipaApp::from(self)
45+
}
46+
}
47+
48+
pub struct UtoipaApp<T>(actix_web::App<T>, Rc<utoipa::openapi::OpenApi>);
49+
50+
impl<T> From<actix_web::App<T>> for UtoipaApp<T> {
51+
fn from(value: actix_web::App<T>) -> Self {
52+
#[derive(OpenApi)]
53+
struct Api;
54+
UtoipaApp(value, Rc::new(Api::openapi()))
55+
}
56+
}
57+
58+
impl<T> UtoipaApp<T>
59+
where
60+
T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
61+
{
62+
/// Passthrough implementation for [`actix_web::App::app_data`].
63+
pub fn app_data<U: 'static>(self, data: U) -> Self {
64+
let app = self.0.app_data(data);
65+
Self(app, self.1)
66+
}
67+
68+
/// Passthrough implementation for [`actix_web::App::data_factory`].
69+
pub fn data_factory<F, Out, D, E>(self, data: F) -> Self
70+
where
71+
F: Fn() -> Out + 'static,
72+
Out: Future<Output = Result<D, E>> + 'static,
73+
D: 'static,
74+
E: std::fmt::Debug,
75+
{
76+
let app = self.0.data_factory(data);
77+
78+
Self(app, self.1)
79+
}
80+
81+
/// Passthrough implementation for [`actix_web::App::configure`].
82+
pub fn configure<F>(mut self, f: F) -> Self
83+
where
84+
F: FnOnce(&mut ServiceConfig),
85+
{
86+
// TODO get OpenAPI paths????
87+
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect(
88+
"OpenApi should not have more than one reference when building App with `configure`",
89+
);
90+
91+
let app = self.0.configure(|config| {
92+
let mut service_config = ServiceConfig::new(config);
93+
94+
f(&mut service_config);
95+
96+
let ServiceConfig(_, paths) = service_config;
97+
api.paths.paths.extend(paths.take().paths);
98+
});
99+
100+
Self(app, self.1)
101+
}
102+
103+
/// Passthrough implementation for [`actix_web::App::route`].
104+
pub fn route(self, path: &str, route: actix_web::Route) -> Self {
105+
let app = self.0.route(path, route);
106+
107+
Self(app, self.1)
108+
}
109+
110+
/// Passthrough implementation for [`actix_web::App::service`].
111+
pub fn service<F>(mut self, factory: F) -> Self
112+
where
113+
F: HttpServiceFactory + PathsFactory + 'static,
114+
{
115+
let paths = factory.paths();
116+
117+
// TODO should this be `make_mut`?
118+
let api = Rc::<utoipa::openapi::OpenApi>::get_mut(&mut self.1).expect(
119+
"OpenApi should not have more than one reference when building App with `service`",
120+
);
121+
122+
api.paths.paths.extend(paths.paths);
123+
let app = self.0.service(factory);
124+
125+
Self(app, self.1)
126+
}
127+
128+
pub fn openapi_service<O, F>(self, factory: F) -> Self
129+
where
130+
F: FnOnce(Rc<utoipa::openapi::OpenApi>) -> O,
131+
O: HttpServiceFactory + 'static,
132+
{
133+
let service = factory(self.1.clone());
134+
let app = self.0.service(service);
135+
Self(app, self.1)
136+
}
137+
138+
/// Passthrough implementation for [`actix_web::App::default_service`].
139+
pub fn default_service<F, U>(self, svc: F) -> Self
140+
where
141+
F: IntoServiceFactory<U, ServiceRequest>,
142+
U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
143+
+ 'static,
144+
U::InitError: fmt::Debug,
145+
{
146+
Self(self.0.default_service(svc), self.1)
147+
}
148+
149+
/// Passthrough implementation for [`actix_web::App::external_resource`].
150+
pub fn external_resource<N, U>(self, name: N, url: U) -> Self
151+
where
152+
N: AsRef<str>,
153+
U: AsRef<str>,
154+
{
155+
Self(self.0.external_resource(name, url), self.1)
156+
}
157+
158+
pub fn map<
159+
F: FnOnce(actix_web::App<T>) -> actix_web::App<NF>,
160+
NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
161+
>(
162+
self,
163+
op: F,
164+
) -> UtoipaApp<NF> {
165+
let app = op(self.0);
166+
UtoipaApp(app, self.1)
167+
}
168+
169+
pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) {
170+
(
171+
self.0,
172+
Rc::try_unwrap(self.1).unwrap_or_else(|rc| (*rc).clone()),
173+
)
174+
}
175+
}
176+
177+
#[cfg(test)]
178+
mod tests {
179+
use actix_service::Service;
180+
use actix_web::http::header::{HeaderValue, CONTENT_TYPE};
181+
use actix_web::{get, App};
182+
183+
use super::*;
184+
185+
#[utoipa::path(impl_for = handler2)]
186+
#[get("/handler2")]
187+
async fn handler2() -> &'static str {
188+
"this is message 2"
189+
}
190+
191+
#[utoipa::path(impl_for = handler)]
192+
#[get("/handler")]
193+
async fn handler() -> &'static str {
194+
"this is message"
195+
}
196+
197+
#[utoipa::path(impl_for = handler3)]
198+
#[get("/handler3")]
199+
async fn handler3() -> &'static str {
200+
"this is message 3"
201+
}
202+
203+
#[test]
204+
fn test_app() {
205+
fn config(cfg: &mut service_config::ServiceConfig) {
206+
cfg.service(handler3);
207+
}
208+
209+
let (_, api) = App::new()
210+
.into_utoipa_app()
211+
.service(handler)
212+
.configure(config)
213+
.service(scope::scope("/path-prefix").service(handler2).map(|scope| {
214+
let s = scope.wrap_fn(|req, srv| {
215+
let fut = srv.call(req);
216+
async {
217+
let mut res = fut.await?;
218+
res.headers_mut()
219+
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
220+
Ok(res)
221+
}
222+
});
223+
224+
s
225+
}))
226+
.split_for_parts();
227+
228+
dbg!(api);
229+
// let app = app.service(scope::scope("prefix").service(handler));
230+
231+
// app
232+
}
233+
}

0 commit comments

Comments
 (0)