silent_openapi/
handler.rs1use crate::{OpenApiError, Result, SwaggerUiOptions};
6use async_trait::async_trait;
7use silent::{Handler, Request, Response, StatusCode};
8use utoipa::openapi::OpenApi;
9
10#[derive(Clone)]
18pub struct SwaggerUiHandler {
19 ui_path: String,
21 api_doc_path: String,
23 openapi_json: String,
25 options: SwaggerUiOptions,
27}
28
29impl SwaggerUiHandler {
30 pub fn new(ui_path: &str, openapi: OpenApi) -> Result<Self> {
50 let api_doc_path = format!("{}/openapi.json", ui_path.trim_end_matches('/'));
51 let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
52
53 Ok(Self {
54 ui_path: ui_path.to_string(),
55 api_doc_path,
56 openapi_json,
57 options: SwaggerUiOptions::default(),
58 })
59 }
60
61 pub fn with_custom_api_doc_path(
69 ui_path: &str,
70 api_doc_path: &str,
71 openapi: OpenApi,
72 ) -> Result<Self> {
73 let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
74
75 Ok(Self {
76 ui_path: ui_path.to_string(),
77 api_doc_path: api_doc_path.to_string(),
78 openapi_json,
79 options: SwaggerUiOptions::default(),
80 })
81 }
82
83 pub fn with_options(
85 ui_path: &str,
86 openapi: OpenApi,
87 options: SwaggerUiOptions,
88 ) -> Result<Self> {
89 let api_doc_path = format!("{}/openapi.json", ui_path.trim_end_matches('/'));
90 let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
91
92 Ok(Self {
93 ui_path: ui_path.to_string(),
94 api_doc_path,
95 openapi_json,
96 options,
97 })
98 }
99
100 fn matches_path(&self, path: &str) -> bool {
102 path == self.ui_path
107 || path.starts_with(&format!("{}/", self.ui_path))
108 || path == self.api_doc_path
109 }
110
111 async fn handle_openapi_json(&self) -> Result<Response> {
113 let mut response = Response::empty();
114 response.set_status(StatusCode::OK);
115 response.set_header(
116 http::header::CONTENT_TYPE,
117 http::HeaderValue::from_static("application/json; charset=utf-8"),
118 );
119 response.set_body(self.openapi_json.clone().into());
120 Ok(response)
121 }
122
123 async fn handle_ui_redirect(&self) -> Result<Response> {
125 let redirect_url = format!("{}/", self.ui_path);
126 let mut response = Response::empty();
127 response.set_status(StatusCode::MOVED_PERMANENTLY);
128 response.set_header(
129 http::header::LOCATION,
130 http::HeaderValue::from_str(&redirect_url)
131 .unwrap_or_else(|_| http::HeaderValue::from_static("/")),
132 );
133 Ok(response)
134 }
135
136 async fn handle_ui_resource(&self, path: &str) -> Result<Response> {
138 let relative_path = path
140 .strip_prefix(&format!("{}/", self.ui_path))
141 .unwrap_or("");
142
143 if relative_path.is_empty() || relative_path == "index.html" {
145 return self.serve_swagger_ui_index().await;
146 }
147
148 self.serve_swagger_ui_asset(relative_path).await
150 }
151
152 async fn serve_swagger_ui_index(&self) -> Result<Response> {
154 let html = format!(
156 r#"<!DOCTYPE html>
157<html lang="zh-CN">
158<head>
159 <meta charset="UTF-8">
160 <meta name="viewport" content="width=device-width, initial-scale=1.0">
161 <title>Swagger UI</title>
162 <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui.css" />
163 <link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@5.17.14/favicon-32x32.png" sizes="32x32" />
164 <style>
165 html {{
166 box-sizing: border-box;
167 overflow: -moz-scrollbars-vertical;
168 overflow-y: scroll;
169 }}
170 *, *:before, *:after {{
171 box-sizing: inherit;
172 }}
173 body {{
174 margin:0;
175 background: #fafafa;
176 }}
177 </style>
178</head>
179<body>
180 <div id="swagger-ui"></div>
181
182 <script src="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-bundle.js"></script>
183 <script src="https://unpkg.com/swagger-ui-dist@5.17.14/swagger-ui-standalone-preset.js"></script>
184 <script>
185 window.onload = function() {{
186 const ui = SwaggerUIBundle({{
187 url: '{}',
188 dom_id: '#swagger-ui',
189 deepLinking: true,
190 presets: [
191 SwaggerUIBundle.presets.apis,
192 SwaggerUIStandalonePreset
193 ],
194 plugins: [
195 SwaggerUIBundle.plugins.DownloadUrl
196 ],
197 layout: "StandaloneLayout",
198 tryItOutEnabled: {}
199 }})
200 }}
201 </script>
202</body>
203</html>"#,
204 self.api_doc_path,
205 if self.options.try_it_out_enabled {
206 "true"
207 } else {
208 "false"
209 }
210 );
211
212 let mut response = Response::empty();
213 response.set_status(StatusCode::OK);
214 response.set_header(
215 http::header::CONTENT_TYPE,
216 http::HeaderValue::from_static("text/html; charset=utf-8"),
217 );
218 response.set_body(html.into());
219 Ok(response)
220 }
221
222 async fn serve_swagger_ui_asset(&self, _asset_path: &str) -> Result<Response> {
224 let mut response = Response::empty();
227 response.set_status(StatusCode::NOT_FOUND);
228 response.set_body("Asset not found".into());
229 Ok(response)
230 }
231
232 pub fn into_route(self) -> silent::prelude::Route {
239 use silent::prelude::{HandlerGetter, Method, Route};
240 use std::sync::Arc;
241
242 let mount = self.ui_path.trim_start_matches('/');
243
244 let base = Route::new(mount)
245 .insert_handler(Method::GET, Arc::new(self.clone()))
246 .insert_handler(Method::HEAD, Arc::new(self.clone()))
247 .append(
248 Route::new("<path:**>")
249 .insert_handler(Method::GET, Arc::new(self.clone()))
250 .insert_handler(Method::HEAD, Arc::new(self)),
251 );
252
253 Route::new("").append(base)
254 }
255}
256
257impl silent::prelude::RouterAdapt for SwaggerUiHandler {
259 fn into_router(self) -> silent::prelude::Route {
260 self.into_route()
261 }
262}
263
264#[async_trait]
265impl Handler for SwaggerUiHandler {
266 async fn call(&self, req: Request) -> silent::Result<Response> {
267 let path = req.uri().path();
268
269 if !self.matches_path(path) {
271 return Err(silent::SilentError::NotFound);
272 }
273
274 let result = if path == self.api_doc_path {
275 self.handle_openapi_json().await
277 } else if path == self.ui_path {
278 self.handle_ui_redirect().await
280 } else {
281 self.handle_ui_resource(path).await
283 };
284
285 match result {
286 Ok(response) => Ok(response),
287 Err(e) => {
288 eprintln!("Swagger UI处理错误: {}", e);
289 Err(silent::SilentError::NotFound)
290 }
291 }
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 use utoipa::OpenApi;
299
300 #[derive(OpenApi)]
301 #[openapi(
302 info(title = "Test API", version = "1.0.0"),
303 paths(),
304 components(schemas())
305 )]
306 struct TestApiDoc;
307
308 #[test]
309 fn test_swagger_ui_handler_creation() {
310 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi());
311 assert!(handler.is_ok());
312
313 let handler = handler.unwrap();
314 assert_eq!(handler.ui_path, "/swagger-ui");
315 assert_eq!(handler.api_doc_path, "/swagger-ui/openapi.json");
316 }
317
318 #[test]
319 fn test_path_matching() {
320 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi()).unwrap();
321
322 assert!(handler.matches_path("/swagger-ui"));
323 assert!(handler.matches_path("/swagger-ui/"));
324 assert!(handler.matches_path("/swagger-ui/index.html"));
325 assert!(handler.matches_path("/swagger-ui/openapi.json"));
326 assert!(handler.matches_path("/swagger-ui/any/asset.js"));
327 assert!(!handler.matches_path("/api/users"));
328 assert!(!handler.matches_path("/swagger"));
329 }
330
331 #[tokio::test]
332 async fn test_openapi_json_response() {
333 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi()).unwrap();
334 let response = handler.handle_openapi_json().await.unwrap();
335
336 assert!(response.headers().get(http::header::CONTENT_TYPE).is_some());
339 }
340
341 #[tokio::test]
342 async fn test_call_openapi_json_via_dispatch() {
343 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
344 let mut req = Request::empty();
345 *req.uri_mut() = http::Uri::from_static("http://localhost/docs/openapi.json");
346 let resp = handler.call(req).await.unwrap();
347 assert!(
348 resp.headers()
349 .get(http::header::CONTENT_TYPE)
350 .map(|v| v.to_str().unwrap_or("").contains("application/json"))
351 .unwrap_or(false)
352 );
353 }
354
355 #[tokio::test]
356 async fn test_call_redirect_and_asset() {
357 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
358 let mut req = Request::empty();
360 *req.uri_mut() = http::Uri::from_static("http://localhost/docs");
361 let resp = handler.call(req).await.unwrap();
362 assert!(resp.headers().get(http::header::LOCATION).is_some());
363
364 let mut req2 = Request::empty();
366 *req2.uri_mut() = http::Uri::from_static("http://localhost/docs/unknown.css");
367 let _resp2 = handler.call(req2).await.unwrap();
368 }
369
370 #[tokio::test]
371 async fn test_handle_ui_resource_index_html() {
372 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
373 let resp = handler
374 .handle_ui_resource("/docs/index.html")
375 .await
376 .unwrap();
377 let ct = resp.headers().get(http::header::CONTENT_TYPE).unwrap();
378 assert!(ct.to_str().unwrap_or("").contains("text/html"));
379 }
380
381 #[tokio::test]
382 async fn test_head_fallback_via_route() {
383 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
385 let route = handler.into_route();
386 let mut req = Request::empty();
387 *req.method_mut() = http::Method::HEAD;
388 *req.uri_mut() = http::Uri::from_static("http://localhost/docs/openapi.json");
389 let resp = route.call(req).await.unwrap();
390 assert!(resp.headers().get(http::header::CONTENT_TYPE).is_some());
391 }
392}
393
394