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}