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
34pub 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
44pub trait OpenApiExt<T> {
46 type Wrapper;
47
48 fn wrap_api(self) -> Self::Wrapper;
51
52 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 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 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 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}