aide/redoc/
mod.rs

1//! Generate [Redoc] ui. This feature requires the `axum` feature.
2//!
3//! ## Example:
4//!
5//! ```no_run
6//! // Replace some of the `axum::` types with `aide::axum::` ones.
7//! use aide::{
8//!     axum::{
9//!         routing::{get, post},
10//!         ApiRouter, IntoApiResponse,
11//!     },
12//!     openapi::{Info, OpenApi},
13//!     redoc::Redoc,
14//! };
15//! use axum::{Extension, Json};
16//! use schemars::JsonSchema;
17//! use serde::Deserialize;
18//!
19//! // We'll need to derive `JsonSchema` for
20//! // all types that appear in the api documentation.
21//! #[derive(Deserialize, JsonSchema)]
22//! struct User {
23//!     name: String,
24//! }
25//!
26//! async fn hello_user(Json(user): Json<User>) -> impl IntoApiResponse {
27//!     format!("hello {}", user.name)
28//! }
29//!
30//! // Note that this clones the document on each request.
31//! // To be more efficient, we could wrap it into an Arc,
32//! // or even store it as a serialized string.
33//! async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoApiResponse {
34//!     Json(api)
35//! }
36//!
37//! #[tokio::main]
38//! async fn main() {
39//!     let app = ApiRouter::new()
40//!         // generate redoc-ui using the openapi spec route
41//!         .route("/redoc", Redoc::new("/api.json").axum_route())
42//!         // Change `route` to `api_route` for the route
43//!         // we'd like to expose in the documentation.
44//!         .api_route("/hello", post(hello_user))
45//!         // We'll serve our generated document here.
46//!         .route("/api.json", get(serve_api));
47//!
48//!     let mut api = OpenApi {
49//!         info: Info {
50//!             description: Some("an example API".to_string()),
51//!             ..Info::default()
52//!         },
53//!         ..OpenApi::default()
54//!     };
55//!
56//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
57//!
58//!     axum::serve(
59//!         listener,
60//!         app
61//!             // Generate the documentation.
62//!             .finish_api(&mut api)
63//!             // Expose the documentation to the handlers.
64//!             .layer(Extension(api))
65//!             .into_make_service(),
66//!     )
67//!     .await
68//!     .unwrap();
69//! }
70//! ```
71
72/// A wrapper to embed [Redoc](https://redocly.com/) in your app.
73#[must_use]
74pub struct Redoc {
75    title: String,
76    spec_url: String,
77}
78
79impl Redoc {
80    /// Create a new [`Redoc`] wrapper with the given spec url.
81    pub fn new(spec_url: impl Into<String>) -> Self {
82        Self {
83            title: "Redoc".into(),
84            spec_url: spec_url.into(),
85        }
86    }
87
88    /// Set the title of the Redoc page.
89    pub fn with_title(mut self, title: &str) -> Self {
90        self.title = title.into();
91        self
92    }
93
94    /// Build the redoc-ui html page.
95    #[must_use]
96    pub fn html(&self) -> String {
97        format!(
98            r#"<!DOCTYPE html>
99<html lang="en">
100  <head>
101    <meta charset="UTF-8">
102    <title>{title}</title>
103  </head>
104
105  <body>
106    <div id="redoc-container"></div>
107    <script>
108       {redoc_js}
109
110       Redoc.init("{spec_url}", {{
111            scrollYOffset: 50
112       }}, document.getElementById('redoc-container'))
113    </script>
114  </body>
115</html>
116"#,
117            redoc_js = include_str!("../../res/redoc/redoc.standalone.js"),
118            title = self.title,
119            spec_url = self.spec_url
120        )
121    }
122}
123
124#[cfg(feature = "axum")]
125mod axum_impl {
126    use crate::axum::{
127        routing::{get, ApiMethodRouter},
128        AxumOperationHandler,
129    };
130    use crate::redoc::get_static_str;
131    use axum::response::Html;
132
133    impl super::Redoc {
134        /// Returns an [`ApiMethodRouter`] to expose the Redoc UI.
135        ///
136        /// # Examples
137        ///
138        /// ```
139        /// # use aide::axum::{ApiRouter, routing::get};
140        /// # use aide::redoc::Redoc;
141        /// ApiRouter::<()>::new()
142        ///     .route("/docs", Redoc::new("/openapi.json").axum_route());
143        /// ```
144        pub fn axum_route<S>(&self) -> ApiMethodRouter<S>
145        where
146            S: Clone + Send + Sync + 'static,
147        {
148            get(self.axum_handler())
149        }
150
151        /// Returns an axum [`Handler`](axum::handler::Handler) that can be used
152        /// with API routes.
153        ///
154        /// # Examples
155        ///
156        /// ```
157        /// # use aide::axum::{ApiRouter, routing::get_with};
158        /// # use aide::redoc::Redoc;
159        /// ApiRouter::<()>::new().api_route(
160        ///     "/docs",
161        ///     get_with(Redoc::new("/openapi.json").axum_handler(), |op| {
162        ///         op.description("This documentation page.")
163        ///     }),
164        /// );
165        /// ```
166        #[must_use]
167        pub fn axum_handler<S>(
168            &self,
169        ) -> impl AxumOperationHandler<(), Html<&'static str>, ((),), S> {
170            let html = self.html();
171            // This string will be used during the entire lifetime of the program
172            // so it's safe to leak it
173            // we can't use once_cell::sync::Lazy because it will cache the first access to the function,
174            // so you won't be able to have multiple instances of Redoc
175            // e.g. /v1/docs and /v2/docs
176            // Without caching we will have to clone whole html string on each request
177            // which will use 3GiBs of RAM for 200+ concurrent requests
178            let html: &'static str = get_static_str(html);
179
180            move || async move { Html(html) }
181        }
182    }
183}
184
185fn get_static_str(string: String) -> &'static str {
186    let static_str = Box::leak(string.into_boxed_str());
187    static_str
188}