utoipa_actix_web/
lib.rs

1//! This crate implements necessary bindings for automatically collecting `paths` and `schemas` recursively from Actix Web
2//! `App`, `Scope` and `ServiceConfig`. It provides natural API reducing duplication and support for scopes while generating
3//! OpenAPI specification without the need to declare `paths` and `schemas` to `#[openapi(...)]` attribute of `OpenApi` derive.
4//!
5//! Currently only `service(...)` calls supports automatic collection of schemas and paths. Manual routes via `route(...)` or
6//! `Route::new().to(...)` is not supported.
7//!
8//! ## Install
9//!
10//! Add dependency declaration to `Cargo.toml`.
11//!
12//! ```toml
13//! [dependencies]
14//! utoipa-actix-web = "0.1"
15//! ```
16//!
17//! ## Examples
18//!
19//! _**Collect handlers annotated with `#[utoipa::path]` recursively from `service(...)` calls to compose OpenAPI spec.**_
20//!
21//! ```rust
22//! use actix_web::web::Json;
23//! use actix_web::{get, App};
24//! use utoipa_actix_web::{scope, AppExt};
25//!
26//! #[derive(utoipa::ToSchema, serde::Serialize)]
27//! struct User {
28//!     id: i32,
29//! }
30//!
31//! #[utoipa::path(responses((status = OK, body = User)))]
32//! #[get("/user")]
33//! async fn get_user() -> Json<User> {
34//!     Json(User { id: 1 })
35//! }
36//!
37//! let (_, mut api) = App::new()
38//!     .into_utoipa_app()
39//!     .service(scope::scope("/api/v1").service(get_user))
40//!     .split_for_parts();
41//! ```
42
43#![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
63/// This trait is used to unify OpenAPI items collection from types implementing this trait.
64pub trait OpenApiFactory {
65    /// Get OpenAPI paths.
66    fn paths(&self) -> utoipa::openapi::path::Paths;
67    /// Collect schema reference and append them to the _`schemas`_.
68    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
115/// Extends [`actix_web::App`] with `utoipa` related functionality.
116pub trait AppExt<T> {
117    /// Convert's this [`actix_web::App`] to [`UtoipaApp`].
118    ///
119    /// See usage from [`UtoipaApp`][struct@UtoipaApp]
120    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
129/// Wrapper type for [`actix_web::App`] and [`utoipa::openapi::OpenApi`].
130///
131/// [`UtoipaApp`] behaves exactly same way as [`actix_web::App`] but allows automatic _`schema`_ and
132/// _`path`_ collection from `service(...)` calls directly or via [`ServiceConfig::service`].
133///
134/// It exposes typical methods from [`actix_web::App`] and provides custom [`UtoipaApp::map`]
135/// method to add additional configuration options to wrapper [`actix_web::App`].
136///
137/// This struct need be instantiated from [`actix_web::App`] by calling `.into_utoipa_app()`
138/// because we do not have access to _`actix_web::App<T>`_ generic argument and the _`App`_ does
139/// not provide any default implementation.
140///
141/// # Examples
142///
143/// _**Create new [`UtoipaApp`] instance.**_
144/// ```rust
145/// # use utoipa_actix_web::{AppExt, UtoipaApp};
146/// # use actix_web::App;
147/// let utoipa_app = App::new().into_utoipa_app();
148/// ```
149///
150/// _**Convert `actix_web::App<T>` to `UtoipaApp<T>`.**_
151/// ```rust
152/// # use utoipa_actix_web::{AppExt, UtoipaApp};
153/// # use actix_web::App;
154/// let a: UtoipaApp<_> = actix_web::App::new().into();
155/// ```
156pub 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    /// Replace the wrapped [`utoipa::openapi::OpenApi`] with given _`openapi`_.
171    ///
172    /// This is useful to prepend OpenAPI doc generated with [`UtoipaApp`]
173    /// with content that cannot be provided directly via [`UtoipaApp`].
174    ///
175    /// # Examples
176    ///
177    /// _**Replace wrapped [`utoipa::openapi::OpenApi`] with custom one.**_
178    /// ```rust
179    /// # use utoipa_actix_web::{AppExt, UtoipaApp};
180    /// # use actix_web::App;
181    /// # use utoipa::OpenApi;
182    /// #[derive(OpenApi)]
183    /// #[openapi(info(title = "Api title"))]
184    /// struct Api;
185    ///
186    /// let _ = actix_web::App::new().into_utoipa_app().openapi(Api::openapi());
187    /// ```
188    pub fn openapi(mut self, openapi: utoipa::openapi::OpenApi) -> Self {
189        self.1 = openapi;
190
191        self
192    }
193
194    /// Passthrough implementation for [`actix_web::App::app_data`].
195    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    /// Passthrough implementation for [`actix_web::App::data_factory`].
201    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    /// Extended version of [`actix_web::App::configure`] which handles _`schema`_ and _`path`_
214    /// collection from [`ServiceConfig`] into the wrapped [`utoipa::openapi::OpenApi`] instance.
215    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    /// Passthrough implementation for [`actix_web::App::route`].
239    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    /// Extended version of [`actix_web::App::service`] method which handles _`schema`_ and _`path`_
246    /// collection from [`HttpServiceFactory`].
247    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    /// Helper method to serve wrapped [`utoipa::openapi::OpenApi`] via [`HttpServiceFactory`].
273    ///
274    /// This method functions as a convenience to serve the wrapped OpenAPI spec alternatively to
275    /// first call [`UtoipaApp::split_for_parts`] and then calling [`actix_web::App::service`].
276    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    /// Passthrough implementation for [`actix_web::App::default_service`].
287    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    /// Passthrough implementation for [`actix_web::App::external_resource`].
298    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    /// Convenience method to add custom configuration to [`actix_web::App`] that is not directly
307    /// exposed via [`UtoipaApp`]. This could for example be adding middlewares.
308    ///
309    /// # Examples
310    ///
311    /// _**Add middleware via `map` method.**_
312    ///
313    /// ```rust
314    /// # use utoipa_actix_web::{AppExt, UtoipaApp};
315    /// # use actix_web::App;
316    /// # use actix_service::Service;
317    /// # use actix_web::http::header::{HeaderValue, CONTENT_TYPE};
318    ///  let _ = App::new()
319    ///     .into_utoipa_app()
320    ///     .map(|app| {
321    ///            app.wrap_fn(|req, srv| {
322    ///                let fut = srv.call(req);
323    ///                async {
324    ///                    let mut res = fut.await?;
325    ///                    res.headers_mut()
326    ///                        .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
327    ///                    Ok(res)
328    ///                }
329    ///            })
330    ///        });
331    /// ```
332    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    /// Split this [`UtoipaApp`] into parts returning tuple of [`actix_web::App`] and
344    /// [`utoipa::openapi::OpenApi`] of this instance.
345    pub fn split_for_parts(self) -> (actix_web::App<T>, utoipa::openapi::OpenApi) {
346        (self.0, self.1)
347    }
348
349    /// Converts this [`UtoipaApp`] into the wrapped [`actix_web::App`].
350    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}