paperclip_actix/
app.rs

1#![allow(clippy::return_self_not_must_use)]
2
3extern crate actix_service2 as actix_service;
4extern crate actix_web4 as actix_web;
5
6#[cfg(feature = "rapidoc")]
7use super::RAPIDOC;
8#[cfg(feature = "swagger-ui")]
9use super::SWAGGER_DIST;
10use super::{
11    web::{Route, RouteWrapper, ServiceConfig},
12    Mountable,
13};
14use actix_service::ServiceFactory;
15#[cfg(feature = "swagger-ui")]
16use actix_web::HttpRequest;
17use actix_web::{
18    body::MessageBody,
19    dev::{HttpServiceFactory, ServiceRequest, ServiceResponse, Transform},
20    Error, HttpResponse,
21};
22use futures::future::{ok as fut_ok, Ready};
23use paperclip_core::v2::models::{DefaultApiRaw, SecurityScheme};
24#[cfg(feature = "rapidoc")]
25use tinytemplate::TinyTemplate;
26
27use std::{
28    collections::BTreeMap,
29    fmt::Debug,
30    future::Future,
31    sync::{Arc, RwLock},
32};
33
34/// Wrapper for [`actix_web::App`](https://docs.rs/actix-web/*/actix_web/struct.App.html).
35pub struct App<T> {
36    spec: Arc<RwLock<DefaultApiRaw>>,
37    #[cfg(feature = "v3")]
38    spec_v3: Option<Arc<RwLock<openapiv3::OpenAPI>>>,
39    #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
40    spec_path: Option<String>,
41    inner: Option<actix_web::App<T>>,
42}
43
44/// Extension trait for actix-web applications.
45pub trait OpenApiExt<T> {
46    type Wrapper;
47
48    /// Consumes this app and produces its wrapper to start tracking
49    /// paths and their corresponding operations.
50    fn wrap_api(self) -> Self::Wrapper;
51
52    /// Same as `wrap_api` initializing with provided specification
53    /// defaults. Useful for defining Api properties outside of definitions and
54    /// paths.
55    fn wrap_api_with_spec(self, spec: DefaultApiRaw) -> Self::Wrapper;
56}
57
58impl<T> OpenApiExt<T> for actix_web::App<T> {
59    type Wrapper = App<T>;
60
61    fn wrap_api(self) -> Self::Wrapper {
62        App {
63            spec: Arc::new(RwLock::new(DefaultApiRaw::default())),
64            #[cfg(feature = "v3")]
65            spec_v3: None,
66            #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
67            spec_path: None,
68            inner: Some(self),
69        }
70    }
71
72    fn wrap_api_with_spec(self, spec: DefaultApiRaw) -> Self::Wrapper {
73        App {
74            spec: Arc::new(RwLock::new(spec)),
75            #[cfg(feature = "v3")]
76            spec_v3: None,
77            #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
78            spec_path: None,
79            inner: Some(self),
80        }
81    }
82}
83
84impl<T> App<T>
85where
86    T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
87{
88    /// Proxy for [`actix_web::App::data_factory`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.data_factory).
89    ///
90    /// **NOTE:** This doesn't affect spec generation.
91    pub fn data_factory<F, Out, D, E>(mut self, data: F) -> Self
92    where
93        F: Fn() -> Out + 'static,
94        Out: Future<Output = Result<D, E>> + 'static,
95        D: 'static,
96        E: Debug,
97    {
98        self.inner = self.inner.take().map(|a| a.data_factory(data));
99        self
100    }
101
102    /// Proxy for [`actix_web::App::app_data`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.app_data).
103    ///
104    /// **NOTE:** This doesn't affect spec generation.
105    pub fn app_data<U: 'static>(mut self, data: U) -> Self {
106        self.inner = self.inner.take().map(|a| a.app_data(data));
107        self
108    }
109
110    /// Wrapper for [`actix_web::App::configure`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.configure).
111    pub fn configure<F>(mut self, f: F) -> Self
112    where
113        F: FnOnce(&mut ServiceConfig),
114    {
115        self.inner = self.inner.take().map(|s| {
116            s.configure(|c| {
117                let mut cfg = ServiceConfig::from(c);
118                f(&mut cfg);
119                self.update_from_mountable(&mut cfg);
120            })
121        });
122        self
123    }
124
125    /// Wrapper for [`actix_web::App::route`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.route).
126    pub fn route(mut self, path: &str, route: Route) -> Self {
127        let mut w = RouteWrapper::from(path, route);
128        self.update_from_mountable(&mut w);
129        self.inner = self.inner.take().map(|a| a.route(path, w.inner));
130        self
131    }
132
133    /// Wrapper for [`actix_web::App::service`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.service).
134    pub fn service<F>(mut self, mut factory: F) -> Self
135    where
136        F: Mountable + HttpServiceFactory + 'static,
137    {
138        self.update_from_mountable(&mut factory);
139        self.inner = self.inner.take().map(|a| a.service(factory));
140        self
141    }
142
143    /// Proxy for [`actix_web::App::default_service`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.default_service).
144    ///
145    /// **NOTE:** This doesn't affect spec generation.
146    pub fn default_service<F, U>(mut self, f: F) -> Self
147    where
148        F: actix_service::IntoServiceFactory<U, ServiceRequest>,
149        U: ServiceFactory<
150                ServiceRequest,
151                Config = (),
152                Response = ServiceResponse,
153                Error = Error,
154                InitError = (),
155            > + 'static,
156        U::InitError: Debug,
157    {
158        self.inner = self.inner.take().map(|a| a.default_service(f));
159        self
160    }
161
162    /// Proxy for [`actix_web::App::external_resource`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.external_resource).
163    ///
164    /// **NOTE:** This doesn't affect spec generation.
165    pub fn external_resource<N, U>(mut self, name: N, url: U) -> Self
166    where
167        N: AsRef<str>,
168        U: AsRef<str>,
169    {
170        self.inner = self.inner.take().map(|a| a.external_resource(name, url));
171        self
172    }
173
174    /// Proxy for [`actix_web::web::App::wrap`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.wrap).
175    ///
176    /// **NOTE:** This doesn't affect spec generation.
177    pub fn wrap<M, B>(
178        mut self,
179        mw: M,
180    ) -> App<
181        impl ServiceFactory<
182            ServiceRequest,
183            Config = (),
184            Response = ServiceResponse<B>,
185            Error = Error,
186            InitError = (),
187        >,
188    >
189    where
190        M: Transform<
191                T::Service,
192                ServiceRequest,
193                Response = ServiceResponse<B>,
194                Error = Error,
195                InitError = (),
196            > + 'static,
197        B: MessageBody,
198    {
199        App {
200            spec: self.spec,
201            #[cfg(feature = "v3")]
202            spec_v3: self.spec_v3,
203            #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
204            spec_path: None,
205            inner: self.inner.take().map(|a| a.wrap(mw)),
206        }
207    }
208
209    /// Proxy for [`actix_web::web::App::wrap_fn`](https://docs.rs/actix-web/*/actix_web/struct.App.html#method.wrap_fn).
210    ///
211    /// **NOTE:** This doesn't affect spec generation.
212    pub fn wrap_fn<F, R, B>(
213        mut self,
214        mw: F,
215    ) -> App<
216        impl ServiceFactory<
217            ServiceRequest,
218            Config = (),
219            Response = ServiceResponse<B>,
220            Error = Error,
221            InitError = (),
222        >,
223    >
224    where
225        F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
226        R: Future<Output = Result<ServiceResponse<B>, Error>>,
227        B: MessageBody,
228    {
229        App {
230            spec: self.spec,
231            #[cfg(feature = "v3")]
232            spec_v3: self.spec_v3,
233            #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
234            spec_path: None,
235            inner: self.inner.take().map(|a| a.wrap_fn(mw)),
236        }
237    }
238
239    /// Mounts the specification for all operations and definitions
240    /// recorded by the wrapper and serves them in the given path
241    /// as a JSON.
242    pub fn with_json_spec_at(mut self, path: &str) -> Self {
243        #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
244        {
245            self.spec_path = Some(path.to_owned());
246        }
247
248        self.inner = self.inner.take().map(|a| {
249            a.service(
250                actix_web::web::resource(path)
251                    .route(actix_web::web::get().to(SpecHandler(self.spec.clone()))),
252            )
253        });
254        self
255    }
256
257    #[cfg(feature = "v3")]
258    /// Converts the generated v2 specification to v3 and then
259    /// mounts the v3 specification for all operations and definitions
260    /// recorded by the wrapper and serves them in the given path
261    /// as a JSON.
262    pub fn with_json_spec_v3_at(mut self, path: &str) -> Self {
263        #[cfg(any(feature = "swagger-ui", feature = "rapidoc"))]
264        {
265            self.spec_path = Some(path.to_owned());
266        }
267
268        let spec_v3 = if let Some(spec_v3) = &self.spec_v3 {
269            spec_v3.clone()
270        } else {
271            let spec_v3 = Arc::new(RwLock::new(openapiv3::OpenAPI::default()));
272            self.spec_v3 = Some(spec_v3.clone());
273            spec_v3
274        };
275        self.inner = self.inner.take().map(|a| {
276            a.service(
277                actix_web::web::resource(path)
278                    .route(actix_web::web::get().to(SpecHandlerV3(spec_v3.clone()))),
279            )
280        });
281        self
282    }
283
284    /// Calls the given function with `App` and JSON `Value` representing your API
285    /// specification **built until now**.
286    ///
287    /// **NOTE:** Unlike `with_json_spec_at`, this only has the API spec built until
288    /// this function call. Any route handler added after this call won't affect the
289    /// spec. So, it's important to call this function after adding all route handlers.
290    pub fn with_raw_json_spec<F>(self, mut call: F) -> Self
291    where
292        F: FnMut(Self, serde_json::Value) -> Self,
293    {
294        let spec = serde_json::to_value(&*self.spec.read().unwrap()).expect("generating json spec");
295        call(self, spec)
296    }
297
298    #[cfg(feature = "v3")]
299    /// Calls the given function with `App` and JSON `Value` representing your API
300    /// v2 specification **built until now** which is converted to v3.
301    ///
302    /// **NOTE:** Unlike `with_json_spec_at`, this only has the API spec built until
303    /// this function call. Any route handler added after this call won't affect the
304    /// spec. So, it's important to call this function after adding all route handlers.
305    pub fn with_raw_json_spec_v3<F>(self, mut call: F) -> Self
306    where
307        F: FnMut(Self, serde_json::Value) -> Self,
308    {
309        let v3 = paperclip_core::v3::openapiv2_to_v3(self.spec.read().unwrap().clone());
310        let spec = serde_json::to_value(v3).expect("generating json spec");
311        call(self, spec)
312    }
313
314    /// Exposes the previously built JSON specification with Swagger UI at the given path
315    ///
316    /// **NOTE:** you **MUST** call with_json_spec_at before calling this function
317    #[cfg(feature = "swagger-ui")]
318    pub fn with_swagger_ui_at(mut self, path: &str) -> Self {
319        let spec_path = self.spec_path.clone().expect(
320            "Specification not set, be sure to call `with_json_spec_at` before this function",
321        );
322
323        let path: String = path.into();
324        // Grab any file request from the documentation UI path and fetch it from SWAGGER_DIST
325        // E.g: js, html, svg and etc.
326        let regex_path = format!("{}/{{filename:.*}}", path);
327
328        self.inner = self.inner.take().map(|a| {
329            a.service(
330                actix_web::web::resource([regex_path.to_owned(), path.clone()]).route(
331                    actix_web::web::get().to(move |request: HttpRequest| {
332                        let path = path.clone();
333                        let spec_path = spec_path.clone();
334                        async move {
335                            let filename = request.match_info().query("filename");
336                            if filename.is_empty() && request.query_string().is_empty() {
337                                let redirect_url = format!("{}/index.html?url={}", path, spec_path);
338                                HttpResponse::PermanentRedirect()
339                                    .append_header(("Location", redirect_url))
340                                    .finish()
341                            } else {
342                                let mut response = HttpResponse::Ok().body(
343                                    SWAGGER_DIST
344                                        .get_file(filename)
345                                        .unwrap_or_else(|| {
346                                            panic!("Failed to get file {}", filename)
347                                        })
348                                        .contents(),
349                                );
350                                if let Some(guess_result) = mime_guess::from_path(filename).first()
351                                {
352                                    if let Ok(header) =
353                                        actix_web::http::header::HeaderValue::from_str(
354                                            guess_result.essence_str(),
355                                        )
356                                    {
357                                        response
358                                            .headers_mut()
359                                            .insert(actix_web::http::header::CONTENT_TYPE, header);
360                                    }
361                                }
362                                response
363                            }
364                        }
365                    }),
366                ),
367            )
368        });
369        self
370    }
371
372    /// Exposes the previously built JSON specification with RapiDoc at the given path
373    ///
374    /// **NOTE:** you **MUST** call with_json_spec_at before calling this function
375    #[cfg(feature = "rapidoc")]
376    pub fn with_rapidoc_at(mut self, path: &str) -> Self {
377        let spec_path = self.spec_path.clone().expect(
378            "Specification not set, be sure to call `with_json_spec_at` before this function",
379        );
380
381        let path: String = path.trim_end_matches('/').into();
382
383        let rapidoc = RAPIDOC
384            .get_file("index.html")
385            .and_then(|file| file.contents_utf8())
386            .unwrap_or_else(|| panic!("Failed to get file RapiDoc UI"));
387        let mut tt = TinyTemplate::new();
388        tt.add_template("index.html", rapidoc).unwrap();
389
390        async fn rapidoc_handler(
391            data: actix_web::web::Data<(TinyTemplate<'_>, String)>,
392        ) -> Result<HttpResponse, Error> {
393            let data = data.into_inner();
394            let (tmpl, spec_path) = data.as_ref();
395            let ctx = serde_json::json!({ "spec_url": spec_path });
396            let s = tmpl.render("index.html", &ctx).map_err(|_| {
397                actix_web::error::ErrorInternalServerError("Error rendering RapiDoc documentation")
398            })?;
399            Ok(HttpResponse::Ok().content_type("text/html").body(s))
400        }
401
402        self.inner = self.inner.take().map(|a| {
403            a.app_data(actix_web::web::Data::new((tt, spec_path)))
404                .service(
405                    actix_web::web::resource(format!("{}/index.html", path))
406                        .route(actix_web::web::get().to(rapidoc_handler)),
407                )
408                .service(
409                    actix_web::web::resource(path).route(actix_web::web::get().to(rapidoc_handler)),
410                )
411        });
412        self
413    }
414
415    /// Builds and returns the `actix_web::App`.
416    pub fn build(self) -> actix_web::App<T> {
417        #[cfg(feature = "v3")]
418        if let Some(v3) = self.spec_v3 {
419            let mut v3 = v3.write().unwrap();
420            *v3 = paperclip_core::v3::openapiv2_to_v3(self.spec.read().unwrap().clone());
421        }
422        self.inner.expect("missing app?")
423    }
424
425    /// Trim's the Api base path from the start of all method paths.
426    /// **NOTE:** much like `with_raw_json_spec` this only has the API spec built until
427    /// this function call. Any route handler added after this call won't have the base path trimmed.
428    /// So, it's important to call this function after adding all route handlers.
429    pub fn trim_base_path(self) -> Self {
430        {
431            let mut spec = self.spec.write().unwrap();
432            let base_path = spec.base_path.clone().unwrap_or_default();
433            spec.paths = spec.paths.iter().fold(BTreeMap::new(), |mut i, (k, v)| {
434                i.insert(
435                    k.trim_start_matches(base_path.as_str()).to_string(),
436                    v.clone(),
437                );
438                i
439            });
440        }
441        self
442    }
443
444    /// Updates the underlying spec with definitions and operations from the given factory.
445    fn update_from_mountable<F>(&mut self, factory: &mut F)
446    where
447        F: Mountable,
448    {
449        let mut api = self.spec.write().unwrap();
450        api.definitions.extend(factory.definitions());
451        SecurityScheme::append_map(
452            factory.security_definitions(),
453            &mut api.security_definitions,
454        );
455        factory.update_operations(&mut api.paths);
456        if cfg!(feature = "normalize") {
457            for map in api.paths.values_mut() {
458                map.normalize();
459            }
460        }
461    }
462}
463
464#[derive(Clone)]
465struct SpecHandler(Arc<RwLock<DefaultApiRaw>>);
466
467impl actix_web::dev::Handler<()> for SpecHandler {
468    type Output = Result<HttpResponse, Error>;
469    type Future = Ready<Self::Output>;
470
471    fn call(&self, _: ()) -> Self::Future {
472        fut_ok(HttpResponse::Ok().json(&*self.0.read().unwrap()))
473    }
474}
475
476#[cfg(feature = "v3")]
477#[derive(Clone)]
478struct SpecHandlerV3(Arc<RwLock<openapiv3::OpenAPI>>);
479
480#[cfg(feature = "v3")]
481impl actix_web::dev::Handler<()> for SpecHandlerV3 {
482    type Output = Result<HttpResponse, Error>;
483    type Future = Ready<Self::Output>;
484
485    fn call(&self, _: ()) -> Self::Future {
486        fut_ok(HttpResponse::Ok().json(&*self.0.read().unwrap()))
487    }
488}