utoipa_rapidoc/lib.rs
1#![warn(missing_docs)]
2#![warn(rustdoc::broken_intra_doc_links)]
3#![cfg_attr(doc_cfg, feature(doc_cfg))]
4//! This crate works as a bridge between [utoipa](https://docs.rs/utoipa/latest/utoipa/) and [RapiDoc](https://rapidocweb.com/) OpenAPI visualizer.
5//!
6//! Utoipa-rapidoc provides simple mechanism to transform OpenAPI spec resource to a servable HTML
7//! file which can be served via [predefined framework integration][Self#examples] or used
8//! [standalone][Self#using-standalone] and served manually.
9//!
10//! You may find fullsize examples from utoipa's Github [repository][examples].
11//!
12//! # Crate Features
13//!
14//! * **actix-web** Allows serving [`RapiDoc`] via _**`actix-web`**_.
15//! * **rocket** Allows serving [`RapiDoc`] via _**`rocket`**_.
16//! * **axum** Allows serving [`RapiDoc`] via _**`axum`**_.
17//!
18//! # Install
19//!
20//! Use RapiDoc only without any boiler plate implementation.
21//! ```toml
22//! [dependencies]
23//! utoipa-rapidoc = "6"
24//! ```
25//!
26//! Enable actix-web integration with RapiDoc.
27//! ```toml
28//! [dependencies]
29//! utoipa-rapidoc = { version = "6", features = ["actix-web"] }
30//! ```
31//!
32//! # Using standalone
33//!
34//! Utoipa-rapidoc can be used standalone as simply as creating a new [`RapiDoc`] instance and then
35//! serving it by what ever means available as `text/html` from http handler in your favourite web
36//! framework.
37//!
38//! [`RapiDoc::to_html`] method can be used to convert the [`RapiDoc`] instance to a servable html
39//! file.
40//! ```
41//! # use utoipa_rapidoc::RapiDoc;
42//! # use utoipa::OpenApi;
43//! # use serde_json::json;
44//! # #[derive(OpenApi)]
45//! # #[openapi()]
46//! # struct ApiDoc;
47//! #
48//! let rapidoc = RapiDoc::new("/api-docs/openapi.json");
49//!
50//! // Then somewhere in your application that handles http operation.
51//! // Make sure you return correct content type `text/html`.
52//! let rapidoc_handler = move || {
53//! rapidoc.to_html()
54//! };
55//! ```
56//!
57//! # Customization
58//!
59//! Utoipa-rapidoc can be customized and configured only via [`RapiDoc::custom_html`] method. This
60//! method empowers users to use a custom HTML template to modify the looks of the RapiDoc UI.
61//!
62//! * [All allowed RapiDoc configuration options][rapidoc_api]
63//! * [Default HTML template][rapidoc_quickstart]
64//!
65//! The template should contain _**`$specUrl`**_ variable which will be replaced with user defined
66//! OpenAPI spec url provided with [`RapiDoc::new`] function when creating a new [`RapiDoc`]
67//! instance. Variable will be replaced during [`RapiDoc::to_html`] function execution.
68//!
69//! _**Overriding the HTML template with a custom one.**_
70//! ```rust
71//! # use utoipa_rapidoc::RapiDoc;
72//! # use utoipa::OpenApi;
73//! # use serde_json::json;
74//! # #[derive(OpenApi)]
75//! # #[openapi()]
76//! # struct ApiDoc;
77//! #
78//! let html = "...";
79//! RapiDoc::new("/api-docs/openapi.json").custom_html(html);
80//! ```
81//!
82//! # Examples
83//!
84//! _**Serve [`RapiDoc`] via `actix-web` framework.**_
85//! ```no_run
86//! use actix_web::App;
87//! use utoipa_rapidoc::RapiDoc;
88//!
89//! # use utoipa::OpenApi;
90//! # use std::net::Ipv4Addr;
91//! # #[derive(OpenApi)]
92//! # #[openapi()]
93//! # struct ApiDoc;
94//! App::new()
95//! .service(
96//! RapiDoc::with_openapi("/api-docs/openapi.json", ApiDoc::openapi()).path("/rapidoc")
97//! );
98//! ```
99//!
100//! _**Serve [`RapiDoc`] via `rocket` framework.**_
101//! ```no_run
102//! # use rocket;
103//! use utoipa_rapidoc::RapiDoc;
104//!
105//! # use utoipa::OpenApi;
106//! # #[derive(OpenApi)]
107//! # #[openapi()]
108//! # struct ApiDoc;
109//! rocket::build()
110//! .mount(
111//! "/",
112//! RapiDoc::with_openapi("/api-docs/openapi.json", ApiDoc::openapi()).path("/rapidoc"),
113//! );
114//! ```
115//!
116//! _**Serve [`RapiDoc`] via `axum` framework.**_
117//! ```no_run
118//! use axum::Router;
119//! use utoipa_rapidoc::RapiDoc;
120//! # use utoipa::OpenApi;
121//! # #[derive(OpenApi)]
122//! # #[openapi()]
123//! # struct ApiDoc;
124//! #
125//! # fn inner<S>()
126//! # where
127//! # S: Clone + Send + Sync + 'static,
128//! # {
129//!
130//! let app = Router::<S>::new()
131//! .merge(
132//! RapiDoc::with_openapi("/api-docs/openapi.json", ApiDoc::openapi()).path("/rapidoc")
133//! );
134//! # }
135//! ```
136//!
137//! [rapidoc_api]: <https://rapidocweb.com/api.html>
138//! [examples]: <https://github.com/juhaku/utoipa/tree/master/examples>
139//! [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>
140
141use std::borrow::Cow;
142
143const DEFAULT_HTML: &str = include_str!("../res/rapidoc.html");
144
145/// Is [RapiDoc][rapidoc] UI.
146///
147/// This is an entry point for serving [RapiDoc][rapidoc] via predefined framework integration or
148/// in standalone fashion by calling [`RapiDoc::to_html`] within custom HTTP handler handles
149/// serving the [RapiDoc][rapidoc] UI. See more at [running standalone][standalone]
150///
151/// [rapidoc]: <https://rapidocweb.com>
152/// [standalone]: index.html#using-standalone
153#[non_exhaustive]
154pub struct RapiDoc {
155 #[allow(unused)]
156 path: Cow<'static, str>,
157 spec_url: Cow<'static, str>,
158 html: Cow<'static, str>,
159 #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
160 openapi: Option<utoipa::openapi::OpenApi>,
161}
162
163impl RapiDoc {
164 /// Construct a new [`RapiDoc`] that points to given `spec_url`. Spec url must be valid URL and
165 /// available for RapiDoc to consume.
166 ///
167 /// # Examples
168 ///
169 /// _**Create new [`RapiDoc`].**_
170 ///
171 /// ```
172 /// # use utoipa_rapidoc::RapiDoc;
173 /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json");
174 /// ```
175 pub fn new<U: Into<Cow<'static, str>>>(spec_url: U) -> Self {
176 Self {
177 path: Cow::Borrowed(""),
178 spec_url: spec_url.into(),
179 html: Cow::Borrowed(DEFAULT_HTML),
180 #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
181 openapi: None,
182 }
183 }
184
185 /// Construct a new [`RapiDoc`] with given `spec_url` and `openapi`. The spec url must point to
186 /// the location where the `openapi` will be served.
187 ///
188 /// [`RapiDoc`] is only able to create endpoint that serves the `openapi` JSON for predefined
189 /// frameworks. _**For other frameworks such endpoint must be created manually.**_
190 ///
191 /// # Examples
192 ///
193 /// _**Create new [`RapiDoc`].**_
194 ///
195 /// ```
196 /// # use utoipa_rapidoc::RapiDoc;
197 /// # use utoipa::OpenApi;
198 /// # #[derive(OpenApi)]
199 /// # #[openapi()]
200 /// # struct ApiDoc;
201 /// RapiDoc::with_openapi(
202 /// "/api-docs/openapi.json",
203 /// ApiDoc::openapi()
204 /// );
205 /// ```
206 #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
207 #[cfg_attr(
208 doc_cfg,
209 doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum")))
210 )]
211 pub fn with_openapi<U: Into<Cow<'static, str>>>(
212 spec_url: U,
213 openapi: utoipa::openapi::OpenApi,
214 ) -> Self {
215 Self {
216 path: Cow::Borrowed(""),
217 spec_url: spec_url.into(),
218 html: Cow::Borrowed(DEFAULT_HTML),
219 openapi: Some(openapi),
220 }
221 }
222
223 /// Construct a new [`RapiDoc`] with given `url`, `spec_url` and `openapi`. The `url` defines
224 /// the location where the RapiDoc UI will be served. The spec url must point to the location
225 /// where the `openapi` will be served.
226 ///
227 /// [`RapiDoc`] is only able to create an endpoint that serves the `openapi` JSON for predefined
228 /// frameworks. _**For other frameworks such an endpoint must be created manually.**_
229 ///
230 /// # Examples
231 ///
232 /// _**Create new [`RapiDoc`] with custom location.**_
233 ///
234 /// ```
235 /// # use utoipa_rapidoc::RapiDoc;
236 /// # use utoipa::OpenApi;
237 /// # #[derive(OpenApi)]
238 /// # #[openapi()]
239 /// # struct ApiDoc;
240 /// RapiDoc::with_url(
241 /// "/rapidoc",
242 /// "/api-docs/openapi.json",
243 /// ApiDoc::openapi()
244 /// );
245 /// ```
246 #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
247 #[cfg_attr(
248 doc_cfg,
249 doc(cfg(any(feature = "actix-web", feature = "rocket", feature = "axum")))
250 )]
251 pub fn with_url<U: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>>(
252 url: U,
253 spec_url: S,
254 openapi: utoipa::openapi::OpenApi,
255 ) -> Self {
256 Self {
257 path: url.into(),
258 spec_url: spec_url.into(),
259 html: Cow::Borrowed(DEFAULT_HTML),
260 openapi: Some(openapi),
261 }
262 }
263
264 /// Override the [default HTML template][rapidoc_quickstart] with new one. See
265 /// [customization] for more details.
266 ///
267 /// [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>
268 /// [customization]: index.html#customization
269 pub fn custom_html<H: Into<Cow<'static, str>>>(mut self, html: H) -> Self {
270 self.html = html.into();
271
272 self
273 }
274
275 /// Add `path` the [`RapiDoc`] will be served from.
276 ///
277 /// # Examples
278 ///
279 /// _**Make [`RapiDoc`] servable from `/rapidoc` path.**_
280 /// ```
281 /// # use utoipa_rapidoc::RapiDoc;
282 ///
283 /// RapiDoc::new("https://petstore3.swagger.io/api/v3/openapi.json")
284 /// .path("/rapidoc");
285 /// ```
286 #[cfg(any(feature = "actix-web", feature = "rocket", feature = "axum"))]
287 pub fn path<U: Into<Cow<'static, str>>>(mut self, path: U) -> Self {
288 self.path = path.into();
289
290 self
291 }
292
293 /// Converts this [`RapiDoc`] instance to servable HTML file.
294 ///
295 /// This will replace _**`$specUrl`**_ variable placeholder with the spec
296 /// url provided to the [`RapiDoc`] instance. If HTML template is not overridden with
297 /// [`RapiDoc::custom_html`] then the [default HTML template][rapidoc_quickstart]
298 /// will be used.
299 ///
300 /// See more details in [customization][customization].
301 ///
302 /// [rapidoc_quickstart]: <https://rapidocweb.com/quickstart.html>
303 /// [customization]: index.html#customization
304 pub fn to_html(&self) -> String {
305 self.html.replace("$specUrl", self.spec_url.as_ref())
306 }
307}
308
309mod actix {
310 #![cfg(feature = "actix-web")]
311
312 use actix_web::dev::HttpServiceFactory;
313 use actix_web::guard::Get;
314 use actix_web::web::Data;
315 use actix_web::{HttpResponse, Resource, Responder};
316
317 use crate::RapiDoc;
318
319 impl HttpServiceFactory for RapiDoc {
320 fn register(self, config: &mut actix_web::dev::AppService) {
321 let html = self.to_html();
322
323 async fn serve_rapidoc(rapidoc: Data<String>) -> impl Responder {
324 HttpResponse::Ok()
325 .content_type("text/html")
326 .body(rapidoc.to_string())
327 }
328
329 Resource::new(self.path.as_ref())
330 .guard(Get())
331 .app_data(Data::new(html))
332 .to(serve_rapidoc)
333 .register(config);
334
335 if let Some(openapi) = self.openapi {
336 async fn serve_openapi(openapi: Data<String>) -> impl Responder {
337 HttpResponse::Ok()
338 .content_type("application/json")
339 .body(openapi.into_inner().to_string())
340 }
341
342 Resource::new(self.spec_url.as_ref())
343 .guard(Get())
344 .app_data(Data::new(
345 openapi.to_json().expect("Should serialize to JSON"),
346 ))
347 .to(serve_openapi)
348 .register(config);
349 }
350 }
351 }
352}
353
354mod axum {
355 #![cfg(feature = "axum")]
356
357 use axum::response::Html;
358 use axum::{routing, Json, Router};
359
360 use crate::RapiDoc;
361
362 impl<R> From<RapiDoc> for Router<R>
363 where
364 R: Clone + Send + Sync + 'static,
365 {
366 fn from(value: RapiDoc) -> Self {
367 let html = value.to_html();
368 let openapi = value.openapi;
369
370 let path = value.path.as_ref();
371 let path = if path.is_empty() { "/" } else { path };
372 let mut router =
373 Router::<R>::new().route(path, routing::get(move || async { Html(html) }));
374
375 if let Some(openapi) = openapi {
376 router = router.route(
377 value.spec_url.as_ref(),
378 routing::get(move || async { Json(openapi) }),
379 );
380 }
381
382 router
383 }
384 }
385}
386
387mod rocket {
388 #![cfg(feature = "rocket")]
389
390 use rocket::http::Method;
391 use rocket::response::content::RawHtml;
392 use rocket::route::{Handler, Outcome};
393 use rocket::serde::json::Json;
394 use rocket::{Data, Request, Route};
395
396 use crate::RapiDoc;
397
398 impl From<RapiDoc> for Vec<Route> {
399 fn from(value: RapiDoc) -> Self {
400 let mut routes = vec![Route::new(
401 Method::Get,
402 value.path.as_ref(),
403 RapiDocHandler(value.to_html()),
404 )];
405
406 if let Some(openapi) = value.openapi {
407 routes.push(Route::new(
408 Method::Get,
409 value.spec_url.as_ref(),
410 OpenApiHandler(openapi),
411 ));
412 }
413
414 routes
415 }
416 }
417
418 #[derive(Clone)]
419 struct RapiDocHandler(String);
420
421 #[rocket::async_trait]
422 impl Handler for RapiDocHandler {
423 async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
424 Outcome::from(request, RawHtml(self.0.clone()))
425 }
426 }
427
428 #[derive(Clone)]
429 struct OpenApiHandler(utoipa::openapi::OpenApi);
430
431 #[rocket::async_trait]
432 impl Handler for OpenApiHandler {
433 async fn handle<'r>(&self, request: &'r Request<'_>, _: Data<'r>) -> Outcome<'r> {
434 Outcome::from(request, Json(self.0.clone()))
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 #[test]
442 #[cfg(feature = "axum")]
443 fn test_axum_with_empty_path() {
444 use ::axum::Router;
445 use utoipa::OpenApi;
446
447 use super::RapiDoc;
448
449 #[derive(utoipa::OpenApi)]
450 #[openapi()]
451 struct ApiDoc;
452
453 let _: Router = Router::new().merge(RapiDoc::with_openapi("/rapidoc", ApiDoc::openapi()));
454 }
455}