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/// Provide the OpenAPi Spec either Inline or as Url
154#[derive(Debug, Clone)]
155pub enum OpenApiSource<S: Into<String>> {
156    /// generates the OpenAPI location at {uri_prefix}/openapi.yaml
157    Inline(S),
158    /// generates the OpenAPI location at the given URI
159    InlineWithName {
160        /// OpenAPI definition as String
161        definition: S,
162        /// OpenAPI URI that is used to expose the definition
163        uri: S,
164    },
165    /// uses the given the OpenAPI location
166    Uri(S),
167}
168
169/// Configuration for the API definition
170#[derive(Debug, Clone)]
171pub struct ApiDefinition<S: Into<String> + Clone> {
172    /// URI prefix used for all Axum routes
173    pub uri_prefix: S,
174    /// OpenAPI definition given, either inline of as URL reference
175    pub api_definition: OpenApiSource<S>,
176    /// Optional title of the API, defaults to SwaggerUI
177    pub title: Option<S>,
178}
179
180/// Generate the route for Axum depending on the given configuration
181#[cfg(any(feature = "axum-07", feature = "axum-08"))]
182pub fn generate_routes<S: Into<String> + Clone>(def: ApiDefinition<S>) -> Router {
183    let prefix = def.uri_prefix.into();
184    let prefix2 = format!("{prefix}/");
185    let def2 = def.api_definition.clone();
186    let api_def_uri = match def.api_definition {
187        OpenApiSource::Uri(val) => val.into(),
188        OpenApiSource::Inline(_val) => format!("{prefix}/openapi.yaml"),
189        OpenApiSource::InlineWithName { definition: _, uri } => uri.into(),
190    };
191    let api_def2 = api_def_uri.clone();
192    let api_def3 = api_def_uri.clone();
193    let title = match def.title {
194        Some(val) => val.into(),
195        None => "SwaggerUI".to_string(),
196    };
197    let title2 = title.clone();
198    let mut router = Router::new()
199        .route(
200            &prefix,
201            get(|req: Request| async move { serve_index_axum(api_def_uri, title, req).await }),
202        )
203        .route(
204            &prefix2,
205            get(|req: Request| async move { serve_index_axum(api_def2, title2, req).await }),
206        )
207        .route(&format!("{prefix}/swagger-ui.css"), get(serve_css_axum))
208        .route(
209            &format!("{prefix}/swagger-ui-bundle.js"),
210            get(serve_js_axum),
211        )
212        .route(
213            &format!("{prefix}/swagger-ui.css.map"),
214            get(serve_css_map_axum),
215        )
216        .route(
217            &format!("{prefix}/swagger-ui-bundle.js.map"),
218            get(serve_js_map_axum),
219        );
220    if let OpenApiSource::Inline(source) = def2 {
221        let yaml = source.into();
222        router = router.route(&api_def3, get(|| async { yaml }));
223    } else if let OpenApiSource::InlineWithName { definition, uri: _ } = def2 {
224        let yaml = definition.into();
225        router = router.route(&api_def3, get(|| async { yaml }));
226    }
227    router
228}
229
230/// Generate a scope for the route for Actix depending on the given configuration
231#[cfg(feature = "actix-web")]
232pub fn generate_scope<S: Into<String> + Clone>(def: ApiDefinition<S>) -> impl HttpServiceFactory {
233    let prefix = def.uri_prefix.into();
234    let (uri, yaml) = match def.api_definition {
235        OpenApiSource::Uri(val) => (val.into(), "".to_string()),
236        OpenApiSource::Inline(val) => (format!("{prefix}/openapi.yaml"), val.into()),
237        OpenApiSource::InlineWithName { definition, uri } => (uri.into(), definition.into()),
238    };
239    let title = match def.title {
240        Some(val) => val.into(),
241        None => "SwaggerUI".to_string(),
242    };
243    let source = ApiDefinition::<String> {
244        uri_prefix: prefix.clone(),
245        api_definition: OpenApiSource::InlineWithName {
246            definition: yaml,
247            uri: uri.clone(),
248        },
249        title: Some(title),
250    };
251    web::scope(&prefix)
252        .app_data(web::Data::new(source))
253        .route(
254            "/",
255            web::get().to(
256                |req: HttpRequest, data: web::Data<ApiDefinition<String>>| async move {
257                    let uri = match data.api_definition.clone() {
258                        OpenApiSource::InlineWithName { definition: _, uri } => uri,
259                        _ => "".to_string(),
260                    };
261                    serve_index_actix(uri, data.title.clone().unwrap(), req).await
262                },
263            ),
264        )
265        .route("/swagger-ui.css", web::get().to(serve_css_actix))
266        .route("/swagger-ui-bundle.js", web::get().to(serve_js_actix))
267        .route("/swagger-ui.css.map", web::get().to(serve_css_map_actix))
268        .route(
269            "/swagger-ui-bundle.js.map",
270            web::get().to(serve_js_map_actix),
271        )
272        .route(
273            uri.trim_start_matches(prefix.as_str()),
274            web::get().to(|data: web::Data<ApiDefinition<String>>| async move {
275                let yaml = match data.api_definition.clone() {
276                    OpenApiSource::InlineWithName { definition, uri: _ } => definition,
277                    _ => "".to_string(),
278                };
279                HttpResponse::Ok().body(yaml)
280            }),
281        )
282}