Skip to main content

silent_openapi/
handler.rs

1//! Swagger UI 处理器
2//!
3//! 提供Swagger UI的处理器实现,可以直接作为Silent路由使用。
4
5use crate::{OpenApiError, Result, SwaggerUiOptions};
6use async_trait::async_trait;
7use silent::{Handler, Request, Response, StatusCode};
8use utoipa::openapi::OpenApi;
9
10/// Swagger UI 处理器
11///
12/// 实现了Silent的Handler trait,可以直接添加到路由中。
13/// 负责处理Swagger UI相关的所有请求,包括:
14/// - Swagger UI 静态资源
15/// - OpenAPI 规范JSON
16/// - 重定向到Swagger UI主页
17#[derive(Clone)]
18pub struct SwaggerUiHandler {
19    /// Swagger UI的基础路径
20    ui_path: String,
21    /// OpenAPI JSON的路径
22    api_doc_path: String,
23    /// OpenAPI 规范的JSON字符串
24    openapi_json: String,
25    /// UI 配置
26    options: SwaggerUiOptions,
27}
28
29impl SwaggerUiHandler {
30    /// 创建新的Swagger UI处理器
31    ///
32    /// # 参数
33    ///
34    /// - `ui_path`: Swagger UI的访问路径,如 "/swagger-ui"
35    /// - `openapi`: OpenAPI规范对象
36    ///
37    /// # 示例
38    ///
39    /// ```rust
40    /// use silent_openapi::SwaggerUiHandler;
41    /// use utoipa::OpenApi;
42    ///
43    /// #[derive(OpenApi)]
44    /// #[openapi(paths(), components(schemas()))]
45    /// struct ApiDoc;
46    ///
47    /// let handler = SwaggerUiHandler::new("/swagger-ui", ApiDoc::openapi());
48    /// ```
49    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    /// 使用自定义的API文档路径
62    ///
63    /// # 参数
64    ///
65    /// - `ui_path`: Swagger UI的访问路径
66    /// - `api_doc_path`: OpenAPI JSON的访问路径
67    /// - `openapi`: OpenAPI规范对象
68    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    /// 使用自定义选项创建处理器
84    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    /// 检查请求路径是否匹配
101    fn matches_path(&self, path: &str) -> bool {
102        // 匹配以下情况:
103        // 1. 完全匹配 ui_path (重定向到主页)
104        // 2. 以 ui_path/ 开头的路径 (Swagger UI资源)
105        // 3. 完全匹配 api_doc_path (OpenAPI JSON)
106        path == self.ui_path
107            || path.starts_with(&format!("{}/", self.ui_path))
108            || path == self.api_doc_path
109    }
110
111    /// 处理OpenAPI JSON请求
112    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    /// 处理Swagger UI重定向
124    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    /// 处理Swagger UI资源请求
137    async fn handle_ui_resource(&self, path: &str) -> Result<Response> {
138        // 移除基础路径前缀,获取相对路径
139        let relative_path = path
140            .strip_prefix(&format!("{}/", self.ui_path))
141            .unwrap_or("");
142
143        // 处理根路径请求(显示Swagger UI主页)
144        if relative_path.is_empty() || relative_path == "index.html" {
145            return self.serve_swagger_ui_index().await;
146        }
147
148        // 处理其他静态资源
149        self.serve_swagger_ui_asset(relative_path).await
150    }
151
152    /// 服务Swagger UI主页
153    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    /// 服务Swagger UI静态资源
168    async fn serve_swagger_ui_asset(&self, asset_path: &str) -> Result<Response> {
169        crate::ui_html::serve_asset(asset_path)
170    }
171
172    /// 将处理器转换为可直接挂载的 Route 树
173    ///
174    /// 自动在 `<ui_path>` 下注册以下路由(GET/HEAD):
175    /// - `<ui_path>`
176    /// - `<ui_path>/openapi.json`
177    /// - `<ui_path>/<path:**>`
178    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
197// 允许在 Route::append 直接使用处理器
198impl 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        // 检查路径是否匹配
210        if !self.matches_path(path) {
211            return Err(silent::SilentError::NotFound);
212        }
213
214        let result = if path == self.api_doc_path {
215            // 返回OpenAPI JSON
216            self.handle_openapi_json().await
217        } else if path == self.ui_path {
218            // 重定向到Swagger UI主页
219            self.handle_ui_redirect().await
220        } else {
221            // 处理Swagger UI资源
222            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        // 注意:Silent的Response没有public的status()方法
277        // 这里只验证响应能成功创建
278        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        // 重定向
299        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        // 静态资源404 分支可达
305        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        // 使用 into_route 挂载后,通过 Route 执行 HEAD,验证可达(GET 回退 HEAD)。
324        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// 选项类型在 crate 根导出