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 =
155 crate::ui_html::generate_index_html(&self.ui_path, &self.api_doc_path, &self.options);
156
157 let mut response = Response::empty();
158 response.set_status(StatusCode::OK);
159 response.set_header(
160 http::header::CONTENT_TYPE,
161 http::HeaderValue::from_static("text/html; charset=utf-8"),
162 );
163 response.set_body(html.into());
164 Ok(response)
165 }
166
167 async fn serve_swagger_ui_asset(&self, asset_path: &str) -> Result<Response> {
169 crate::ui_html::serve_asset(asset_path)
170 }
171
172 pub fn into_route(self) -> silent::prelude::Route {
179 use silent::prelude::{HandlerGetter, Method, Route};
180 use std::sync::Arc;
181
182 let mount = self.ui_path.trim_start_matches('/');
183
184 let base = Route::new(mount)
185 .insert_handler(Method::GET, Arc::new(self.clone()))
186 .insert_handler(Method::HEAD, Arc::new(self.clone()))
187 .append(
188 Route::new("<path:**>")
189 .insert_handler(Method::GET, Arc::new(self.clone()))
190 .insert_handler(Method::HEAD, Arc::new(self)),
191 );
192
193 Route::new("").append(base)
194 }
195}
196
197impl silent::prelude::RouterAdapt for SwaggerUiHandler {
199 fn into_router(self) -> silent::prelude::Route {
200 self.into_route()
201 }
202}
203
204#[async_trait]
205impl Handler for SwaggerUiHandler {
206 async fn call(&self, req: Request) -> silent::Result<Response> {
207 let path = req.uri().path();
208
209 if !self.matches_path(path) {
211 return Err(silent::SilentError::NotFound);
212 }
213
214 let result = if path == self.api_doc_path {
215 self.handle_openapi_json().await
217 } else if path == self.ui_path {
218 self.handle_ui_redirect().await
220 } else {
221 self.handle_ui_resource(path).await
223 };
224
225 match result {
226 Ok(response) => Ok(response),
227 Err(e) => {
228 eprintln!("Swagger UI处理错误: {}", e);
229 Err(silent::SilentError::NotFound)
230 }
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use utoipa::OpenApi;
239
240 #[derive(OpenApi)]
241 #[openapi(
242 info(title = "Test API", version = "1.0.0"),
243 paths(),
244 components(schemas())
245 )]
246 struct TestApiDoc;
247
248 #[test]
249 fn test_swagger_ui_handler_creation() {
250 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi());
251 assert!(handler.is_ok());
252
253 let handler = handler.unwrap();
254 assert_eq!(handler.ui_path, "/swagger-ui");
255 assert_eq!(handler.api_doc_path, "/swagger-ui/openapi.json");
256 }
257
258 #[test]
259 fn test_path_matching() {
260 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi()).unwrap();
261
262 assert!(handler.matches_path("/swagger-ui"));
263 assert!(handler.matches_path("/swagger-ui/"));
264 assert!(handler.matches_path("/swagger-ui/index.html"));
265 assert!(handler.matches_path("/swagger-ui/openapi.json"));
266 assert!(handler.matches_path("/swagger-ui/any/asset.js"));
267 assert!(!handler.matches_path("/api/users"));
268 assert!(!handler.matches_path("/swagger"));
269 }
270
271 #[tokio::test]
272 async fn test_openapi_json_response() {
273 let handler = SwaggerUiHandler::new("/swagger-ui", TestApiDoc::openapi()).unwrap();
274 let response = handler.handle_openapi_json().await.unwrap();
275
276 assert!(response.headers().get(http::header::CONTENT_TYPE).is_some());
279 }
280
281 #[tokio::test]
282 async fn test_call_openapi_json_via_dispatch() {
283 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
284 let mut req = Request::empty();
285 *req.uri_mut() = http::Uri::from_static("http://localhost/docs/openapi.json");
286 let resp = handler.call(req).await.unwrap();
287 assert!(
288 resp.headers()
289 .get(http::header::CONTENT_TYPE)
290 .map(|v| v.to_str().unwrap_or("").contains("application/json"))
291 .unwrap_or(false)
292 );
293 }
294
295 #[tokio::test]
296 async fn test_call_redirect_and_asset() {
297 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
298 let mut req = Request::empty();
300 *req.uri_mut() = http::Uri::from_static("http://localhost/docs");
301 let resp = handler.call(req).await.unwrap();
302 assert!(resp.headers().get(http::header::LOCATION).is_some());
303
304 let mut req2 = Request::empty();
306 *req2.uri_mut() = http::Uri::from_static("http://localhost/docs/unknown.css");
307 let _resp2 = handler.call(req2).await.unwrap();
308 }
309
310 #[tokio::test]
311 async fn test_handle_ui_resource_index_html() {
312 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
313 let resp = handler
314 .handle_ui_resource("/docs/index.html")
315 .await
316 .unwrap();
317 let ct = resp.headers().get(http::header::CONTENT_TYPE).unwrap();
318 assert!(ct.to_str().unwrap_or("").contains("text/html"));
319 }
320
321 #[tokio::test]
322 async fn test_head_fallback_via_route() {
323 let handler = SwaggerUiHandler::new("/docs", TestApiDoc::openapi()).unwrap();
325 let route = handler.into_route();
326 let mut req = Request::empty();
327 *req.method_mut() = http::Method::HEAD;
328 *req.uri_mut() = http::Uri::from_static("http://localhost/docs/openapi.json");
329 let resp = route.call(req).await.unwrap();
330 assert!(resp.headers().get(http::header::CONTENT_TYPE).is_some());
331 }
332}
333
334