1#![cfg_attr(doc_cfg, feature(doc_cfg))]
44#![warn(missing_docs)]
45#![warn(rustdoc::broken_intra_doc_links)]
46
47use core::fmt;
48use std::future::Future;
49
50use actix_service::{IntoServiceFactory, ServiceFactory};
51use actix_web::dev::{HttpServiceFactory, ServiceRequest, ServiceResponse};
52use actix_web::Error;
53use utoipa::openapi::PathItem;
54use utoipa::OpenApi;
55
56use self::service_config::ServiceConfig;
57
58pub mod scope;
59pub mod service_config;
60
61pub use scope::scope;
62
63pub trait OpenApiFactory {
65 fn paths(&self) -> utoipa::openapi::path::Paths;
67 fn schemas(
69 &self,
70 schemas: &mut Vec<(
71 String,
72 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
73 )>,
74 );
75}
76
77impl<'t, T: utoipa::Path + utoipa::__dev::SchemaReferences + utoipa::__dev::Tags<'t>> OpenApiFactory
78 for T
79{
80 fn paths(&self) -> utoipa::openapi::path::Paths {
81 let methods = T::methods();
82
83 methods
84 .into_iter()
85 .fold(
86 utoipa::openapi::path::Paths::builder(),
87 |mut builder, method| {
88 let mut operation = T::operation();
89 let other_tags = T::tags();
90 if !other_tags.is_empty() {
91 let tags = operation.tags.get_or_insert(Vec::new());
92 tags.extend(other_tags.into_iter().map(ToString::to_string));
93 };
94
95 let path_item = PathItem::new(method, operation);
96 builder = builder.path(T::path(), path_item);
97
98 builder
99 },
100 )
101 .build()
102 }
103
104 fn schemas(
105 &self,
106 schemas: &mut Vec<(
107 String,
108 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
109 )>,
110 ) {
111 <T as utoipa::__dev::SchemaReferences>::schemas(schemas);
112 }
113}
114
115pub trait AppExt<T> {
117 fn into_utoipa_app(self) -> UtoipaApp<T>;
121}
122
123impl<T> AppExt<T> for actix_web::App<T> {
124 fn into_utoipa_app(self) -> UtoipaApp<T> {
125 UtoipaApp::from(self)
126 }
127}
128
129pub struct UtoipaApp<T>(actix_web::App<T>, utoipa::openapi::OpenApi);
157
158impl<T> From<actix_web::App<T>> for UtoipaApp<T> {
159 fn from(value: actix_web::App<T>) -> Self {
160 #[derive(OpenApi)]
161 struct Api;
162 UtoipaApp(value, Api::openapi())
163 }
164}
165
166impl<T> UtoipaApp<T>
167where
168 T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
169{
170 pub fn openapi(mut self, openapi: utoipa::openapi::OpenApi) -> Self {
189 self.1 = openapi;
190
191 self
192 }
193
194 pub fn app_data<U: 'static>(self, data: U) -> Self {
196 let app = self.0.app_data(data);
197 Self(app, self.1)
198 }
199
200 pub fn data_factory<F, Out, D, E>(self, data: F) -> Self
202 where
203 F: Fn() -> Out + 'static,
204 Out: Future<Output = Result<D, E>> + 'static,
205 D: 'static,
206 E: std::fmt::Debug,
207 {
208 let app = self.0.data_factory(data);
209
210 Self(app, self.1)
211 }
212
213 pub fn configure<F>(self, f: F) -> Self
216 where
217 F: FnOnce(&mut ServiceConfig),
218 {
219 let mut openapi = self.1;
220
221 let app = self.0.configure(|config| {
222 let mut service_config = ServiceConfig::new(config);
223
224 f(&mut service_config);
225
226 let paths = service_config.1.take();
227 openapi.paths.merge(paths);
228 let schemas = service_config.2.take();
229 let components = openapi
230 .components
231 .get_or_insert(utoipa::openapi::Components::new());
232 components.schemas.extend(schemas);
233 });
234
235 Self(app, openapi)
236 }
237
238 pub fn route(self, path: &str, route: actix_web::Route) -> Self {
240 let app = self.0.route(path, route);
241
242 Self(app, self.1)
243 }
244
245 pub fn service<F>(self, factory: F) -> Self
248 where
249 F: HttpServiceFactory + OpenApiFactory + 'static,
250 {
251 let mut schemas = Vec::<(
252 String,
253 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
254 )>::new();
255
256 factory.schemas(&mut schemas);
257 let paths = factory.paths();
258
259 let mut openapi = self.1;
260
261 openapi.paths.merge(paths);
262 let components = openapi
263 .components
264 .get_or_insert(utoipa::openapi::Components::new());
265 components.schemas.extend(schemas);
266
267 let app = self.0.service(factory);
268
269 Self(app, openapi)
270 }
271
272 pub fn openapi_service<O, F>(self, factory: F) -> Self
277 where
278 F: FnOnce(utoipa::openapi::OpenApi) -> O,
279 O: HttpServiceFactory + 'static,
280 {
281 let service = factory(self.1.clone());
282 let app = self.0.service(service);
283 Self(app, self.1)
284 }
285
286 pub fn default_service<F, U>(self, svc: F) -> Self
288 where
289 F: IntoServiceFactory<U, ServiceRequest>,
290 U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error>
291 + 'static,
292 U::InitError: fmt::Debug,
293 {
294 Self(self.0.default_service(svc), self.1)
295 }
296
297 pub fn external_resource<N, U>(self, name: N, url: U) -> Self
299 where
300 N: AsRef<str>,
301 U: AsRef<str>,
302 {
303 Self(self.0.external_resource(name, url), self.1)
304 }
305
306 pub fn map<
333 F: FnOnce(actix_web::App<T>) -> actix_web::App<NF>,
334 NF: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
335 >(
336 self,
337 op: F,
338 ) -> UtoipaApp<NF> {
339 let app = op(self.0);
340 UtoipaApp(app, self.1)
341 }
342
343 pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) {
346 (self.0, self.1)
347 }
348
349 pub fn into_app(self) -> actix_web::App<T> {
351 self.0
352 }
353}
354
355impl<T> From<UtoipaApp<T>> for actix_web::App<T> {
356 fn from(value: UtoipaApp<T>) -> Self {
357 value.0
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 #![allow(unused)]
364
365 use actix_service::Service;
366 use actix_web::guard::{Get, Guard};
367 use actix_web::http::header::{HeaderValue, CONTENT_TYPE};
368 use actix_web::web::{self, Data};
369 use actix_web::{get, App, HttpRequest, HttpResponse};
370 use utoipa::ToSchema;
371
372 use super::*;
373
374 #[derive(ToSchema)]
375 struct Value12 {
376 v: String,
377 }
378
379 #[derive(ToSchema)]
380 struct Value2(i32);
381
382 #[derive(ToSchema)]
383 struct Value1 {
384 bar: Value2,
385 }
386
387 #[derive(ToSchema)]
388 struct ValueValue {
389 value: i32,
390 }
391
392 #[utoipa::path(responses(
393 (status = 200, body = ValueValue)
394 ))]
395 #[get("/handler2")]
396 async fn handler2() -> &'static str {
397 "this is message 2"
398 }
399
400 #[utoipa::path(responses(
401 (status = 200, body = Value12)
402 ))]
403 #[get("/handler")]
404 async fn handler() -> &'static str {
405 "this is message"
406 }
407
408 #[utoipa::path(responses(
409 (status = 200, body = Value1)
410 ))]
411 #[get("/handler3")]
412 async fn handler3() -> &'static str {
413 "this is message 3"
414 }
415
416 mod inner {
417 use actix_web::get;
418 use actix_web::web::Data;
419 use utoipa::ToSchema;
420
421 #[derive(ToSchema)]
422 struct Bar(i32);
423
424 #[derive(ToSchema)]
425 struct Foobar {
426 bar: Bar,
427 }
428
429 #[utoipa::path(responses(
430 (status = 200, body = Foobar)
431 ))]
432 #[get("/inner_handler")]
433 pub async fn inner_handler(_: Data<String>) -> &'static str {
434 "this is message"
435 }
436
437 #[utoipa::path()]
438 #[get("/inner_handler3")]
439 pub async fn inner_handler3(_: Data<String>) -> &'static str {
440 "this is message 3"
441 }
442 }
443
444 #[get("/normal_service")]
445 async fn normal_service() -> &'static str {
446 "str"
447 }
448
449 #[test]
450 fn test_app_generate_correct_openapi() {
451 fn config(cfg: &mut service_config::ServiceConfig) {
452 cfg.service(handler3)
453 .map(|config| config.service(normal_service));
454 }
455
456 let (_, mut api) = App::new()
457 .into_utoipa_app()
458 .service(handler)
459 .configure(config)
460 .service(scope::scope("/path-prefix").service(handler2).map(|scope| {
461 let s = scope.wrap_fn(|req, srv| {
462 let fut = srv.call(req);
463 async {
464 let mut res = fut.await?;
465 res.headers_mut()
466 .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
467 Ok(res)
468 }
469 });
470
471 s
472 }))
473 .service(scope::scope("/api/v1/inner").configure(|cfg| {
474 cfg.service(inner::inner_handler)
475 .service(inner::inner_handler3)
476 .app_data(Data::new(String::new()));
477 }))
478 .split_for_parts();
479 api.info = utoipa::openapi::info::Info::new("title", "version");
480 let json = api.to_pretty_json().expect("OpenAPI is JSON serializable");
481 println!("{json}");
482
483 let expected = include_str!("../testdata/app_generated_openapi");
484 assert_eq!(json.trim(), expected.trim());
485 }
486}