swagger_ui_dist/
lib.rs

1#![deny(missing_docs)]
2//! swagger-ui-dist redistributes the swagger ui
3//!
4//! it repackages the JS/CSS code into axum routes
5//! to allow for an easier implementation
6//!
7//! ```rust
8//! let api_def = ApiDefinition {
9//!   uri_prefix: "/api",
10//!   api_definition: OpenApiSource::Inline(include_str!("petstore.yaml")),
11//!   title: Some("My Super Duper API"),
12//! };
13//! let app = Router::new().merge(swagger_ui_dist::generate_routes(api_def));
14//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
15//! println!("listening on http://localhost:3000/api");
16//! axum::serve(listener, app).await.unwrap();
17//! ```
18
19#[cfg(feature = "actix-web")]
20use actix_web::{dev::HttpServiceFactory, web, HttpRequest, HttpResponse, Responder};
21#[cfg(any(feature = "axum-07", feature = "axum-08"))]
22use axum::{http::header, routing::get, Router};
23#[cfg(feature = "axum-07")]
24use axum_07 as axum;
25#[cfg(feature = "axum-08")]
26use axum_08 as axum;
27#[cfg(any(feature = "axum-07", feature = "axum-08"))]
28use axum_core::{body::Body, extract::Request, response::Response};
29#[cfg(feature = "axum-07")]
30use axum_core_04 as axum_core;
31#[cfg(feature = "axum-08")]
32use axum_core_05 as axum_core;
33
34#[cfg(any(feature = "axum-07", feature = "axum-08"))]
35async fn serve_index_axum(api_def: String, title: String, req: Request) -> Response {
36    let uri = req.uri().to_string();
37
38    let response_str = serve_index(api_def, title, uri);
39
40    Response::builder()
41        .status(200)
42        .header(header::CONTENT_TYPE, "text/html")
43        .body(Body::from(response_str))
44        .unwrap()
45}
46
47#[cfg(feature = "actix-web")]
48async fn serve_index_actix(api_def: String, title: String, req: HttpRequest) -> impl Responder {
49    let uri = req.uri().to_string();
50    let uri = if uri.ends_with("/") {
51        uri.trim_end_matches("/").to_string()
52    } else {
53        uri
54    };
55
56    let response_str = serve_index(api_def, title, uri);
57
58    HttpResponse::Ok()
59        .content_type("text/html")
60        .body(response_str)
61}
62
63fn serve_index(api_def: String, title: String, uri: String) -> String {
64    format!(
65        r#"<!DOCTYPE html>
66<html lang="en">
67<head>
68    <meta charset="utf-8" />
69    <meta name="viewport" content="width=device-width, initial-scale=1" />
70    <title>{title}</title>
71    <link rel="stylesheet" href="{uri}/swagger-ui.css" />
72</head>
73<body>
74<div id="swagger-ui"></div>
75<script src="{uri}/swagger-ui-bundle.js" crossorigin></script>
76<script>
77    window.onload = () => {{
78    window.ui = SwaggerUIBundle({{
79        url: '{api_def}',
80        dom_id: '#swagger-ui',
81    }});
82    }};
83</script>
84</body>
85</html>"#
86    )
87}
88
89#[cfg(any(feature = "axum-07", feature = "axum-08"))]
90async fn serve_js_axum() -> Response {
91    let js: &str = include_str!("../assets/swagger-ui-bundle.js");
92    Response::builder()
93        .status(200)
94        .header("Content-Type", "text/javascript")
95        .body(Body::from(js))
96        .unwrap()
97}
98
99#[cfg(feature = "actix-web")]
100async fn serve_js_actix() -> impl Responder {
101    let js: &str = include_str!("../assets/swagger-ui-bundle.js");
102    HttpResponse::Ok().content_type("text/javascript").body(js)
103}
104
105#[cfg(any(feature = "axum-07", feature = "axum-08"))]
106async fn serve_js_map_axum() -> Response {
107    let js: &str = include_str!("../assets/swagger-ui-bundle.js.map");
108    Response::builder()
109        .status(200)
110        .header("Content-Type", "application/json")
111        .body(Body::from(js))
112        .unwrap()
113}
114
115#[cfg(feature = "actix-web")]
116async fn serve_js_map_actix() -> impl Responder {
117    let js: &str = include_str!("../assets/swagger-ui-bundle.js.map");
118    HttpResponse::Ok().content_type("application/json").body(js)
119}
120
121#[cfg(any(feature = "axum-07", feature = "axum-08"))]
122async fn serve_css_axum() -> Response {
123    let css: &str = include_str!("../assets/swagger-ui.css");
124    Response::builder()
125        .status(200)
126        .header("Content-Type", "text/css")
127        .body(Body::from(css))
128        .unwrap()
129}
130
131#[cfg(feature = "actix-web")]
132async fn serve_css_actix() -> impl Responder {
133    let css: &str = include_str!("../assets/swagger-ui.css");
134    HttpResponse::Ok().content_type("text/css").body(css)
135}
136
137#[cfg(any(feature = "axum-07", feature = "axum-08"))]
138async fn serve_css_map_axum() -> Response {
139    let js: &str = include_str!("../assets/swagger-ui.css.map");
140    Response::builder()
141        .status(200)
142        .header("Content-Type", "application/json")
143        .body(Body::from(js))
144        .unwrap()
145}
146
147#[cfg(feature = "actix-web")]
148async fn serve_css_map_actix() -> impl Responder {
149    let js: &str = include_str!("../assets/swagger-ui.css.map");
150    HttpResponse::Ok().content_type("application/json").body(js)
151}
152
153#[cfg(any(feature = "axum-07", feature = "axum-08"))]
154async fn serve_oauth2_redirect_html_axum() -> Response {
155    let js: &str = include_str!("../assets/oauth2-redirect.html");
156    Response::builder()
157        .status(200)
158        .header("Content-Type", "text/html")
159        .body(Body::from(js))
160        .unwrap()
161}
162
163#[cfg(feature = "actix-web")]
164async fn serve_oauth2_redirect_html_actix() -> impl Responder {
165    let js: &str = include_str!("../assets/oauth2-redirect.html");
166    HttpResponse::Ok().content_type("text/html").body(js)
167}
168
169#[cfg(any(feature = "axum-07", feature = "axum-08"))]
170async fn serve_oauth2_redirect_js_axum() -> Response {
171    let js: &str = include_str!("../assets/oauth2-redirect.js");
172    Response::builder()
173        .status(200)
174        .header("Content-Type", "text/javascript")
175        .body(Body::from(js))
176        .unwrap()
177}
178
179#[cfg(feature = "actix-web")]
180async fn serve_oauth2_redirect_js_actix() -> impl Responder {
181    let js: &str = include_str!("../assets/oauth2-redirect.js");
182    HttpResponse::Ok().content_type("text/javascript").body(js)
183}
184
185/// Provide the OpenAPi Spec either Inline or as Url
186#[derive(Debug, Clone)]
187pub enum OpenApiSource<S: Into<String>> {
188    /// generates the OpenAPI location at {uri_prefix}/openapi.yaml
189    Inline(S),
190    /// generates the OpenAPI location at the given URI
191    InlineWithName {
192        /// OpenAPI definition as String
193        definition: S,
194        /// OpenAPI URI that is used to expose the definition
195        uri: S,
196    },
197    /// uses the given the OpenAPI location
198    Uri(S),
199}
200
201/// Configuration for the API definition
202#[derive(Debug, Clone)]
203pub struct ApiDefinition<S: Into<String> + Clone> {
204    /// URI prefix used for all Axum routes
205    pub uri_prefix: S,
206    /// OpenAPI definition given, either inline of as URL reference
207    pub api_definition: OpenApiSource<S>,
208    /// Optional title of the API, defaults to SwaggerUI
209    pub title: Option<S>,
210}
211
212/// Generate the route for Axum depending on the given configuration
213#[cfg(any(feature = "axum-07", feature = "axum-08"))]
214pub fn generate_routes<S: Into<String> + Clone>(def: ApiDefinition<S>) -> Router {
215    let prefix = def.uri_prefix.into();
216    let prefix2 = format!("{prefix}/");
217    let def2 = def.api_definition.clone();
218    let api_def_uri = match def.api_definition {
219        OpenApiSource::Uri(val) => val.into(),
220        OpenApiSource::Inline(_val) => format!("{prefix}/openapi.yaml"),
221        OpenApiSource::InlineWithName { definition: _, uri } => uri.into(),
222    };
223    let api_def2 = api_def_uri.clone();
224    let api_def3 = api_def_uri.clone();
225    let title = match def.title {
226        Some(val) => val.into(),
227        None => "SwaggerUI".to_string(),
228    };
229    let title2 = title.clone();
230    let mut router = Router::new()
231        .route(
232            &prefix,
233            get(|req: Request| async move { serve_index_axum(api_def_uri, title, req).await }),
234        )
235        .route(
236            &prefix2,
237            get(|req: Request| async move { serve_index_axum(api_def2, title2, req).await }),
238        )
239        .route(&format!("{prefix}/swagger-ui.css"), get(serve_css_axum))
240        .route(
241            &format!("{prefix}/swagger-ui-bundle.js"),
242            get(serve_js_axum),
243        )
244        .route(
245            &format!("{prefix}/swagger-ui.css.map"),
246            get(serve_css_map_axum),
247        )
248        .route(
249            &format!("{prefix}/swagger-ui-bundle.js.map"),
250            get(serve_js_map_axum),
251        )
252        .route(
253            &format!("{prefix}/oauth2-redirect.html"),
254            get(serve_oauth2_redirect_html_axum),
255        )
256        .route(
257            &format!("{prefix}/oauth2-redirect.js"),
258            get(serve_oauth2_redirect_js_axum),
259        );
260    if let OpenApiSource::Inline(source) = def2 {
261        let yaml = source.into();
262        router = router.route(&api_def3, get(|| async { yaml }));
263    } else if let OpenApiSource::InlineWithName { definition, uri: _ } = def2 {
264        let yaml = definition.into();
265        router = router.route(&api_def3, get(|| async { yaml }));
266    }
267    router
268}
269
270/// Generate a scope for the route for Actix depending on the given configuration
271#[cfg(feature = "actix-web")]
272pub fn generate_scope<S: Into<String> + Clone>(def: ApiDefinition<S>) -> impl HttpServiceFactory {
273    let prefix = def.uri_prefix.into();
274    let (uri, yaml) = match def.api_definition {
275        OpenApiSource::Uri(val) => (val.into(), "".to_string()),
276        OpenApiSource::Inline(val) => (format!("{prefix}/openapi.yaml"), val.into()),
277        OpenApiSource::InlineWithName { definition, uri } => (uri.into(), definition.into()),
278    };
279    let title = match def.title {
280        Some(val) => val.into(),
281        None => "SwaggerUI".to_string(),
282    };
283    let source = ApiDefinition::<String> {
284        uri_prefix: prefix.clone(),
285        api_definition: OpenApiSource::InlineWithName {
286            definition: yaml,
287            uri: uri.clone(),
288        },
289        title: Some(title),
290    };
291    web::scope(&prefix)
292        .app_data(web::Data::new(source))
293        .route(
294            "/",
295            web::get().to(
296                |req: HttpRequest, data: web::Data<ApiDefinition<String>>| async move {
297                    let uri = match data.api_definition.clone() {
298                        OpenApiSource::InlineWithName { definition: _, uri } => uri,
299                        _ => "".to_string(),
300                    };
301                    serve_index_actix(uri, data.title.clone().unwrap(), req).await
302                },
303            ),
304        )
305        .route("/swagger-ui.css", web::get().to(serve_css_actix))
306        .route("/swagger-ui-bundle.js", web::get().to(serve_js_actix))
307        .route("/swagger-ui.css.map", web::get().to(serve_css_map_actix))
308        .route(
309            "/swagger-ui-bundle.js.map",
310            web::get().to(serve_js_map_actix),
311        )
312        .route(
313            "/oauth2-redirect.html",
314            web::get().to(serve_oauth2_redirect_html_actix),
315        )
316        .route(
317            "/oauth2-redirect.js",
318            web::get().to(serve_oauth2_redirect_js_actix),
319        )
320        .route(
321            uri.trim_start_matches(prefix.as_str()),
322            web::get().to(|data: web::Data<ApiDefinition<String>>| async move {
323                let yaml = match data.api_definition.clone() {
324                    OpenApiSource::InlineWithName { definition, uri: _ } => definition,
325                    _ => "".to_string(),
326                };
327                HttpResponse::Ok().body(yaml)
328            }),
329        )
330}