use crate::{OpenApiError, Result, SwaggerUiOptions};
use async_trait::async_trait;
use silent::{Handler, MiddleWareHandler, Next, Request, Response, StatusCode};
use utoipa::openapi::OpenApi;
#[derive(Clone)]
pub struct SwaggerUiMiddleware {
ui_path: String,
api_doc_path: String,
openapi_json: String,
options: SwaggerUiOptions,
}
impl SwaggerUiMiddleware {
pub fn new(ui_path: &str, openapi: OpenApi) -> Result<Self> {
let api_doc_path = format!("{}/openapi.json", ui_path.trim_end_matches('/'));
let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
Ok(Self {
ui_path: ui_path.to_string(),
api_doc_path,
openapi_json,
options: SwaggerUiOptions::default(),
})
}
pub fn with_custom_api_doc_path(
ui_path: &str,
api_doc_path: &str,
openapi: OpenApi,
) -> Result<Self> {
let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
Ok(Self {
ui_path: ui_path.to_string(),
api_doc_path: api_doc_path.to_string(),
openapi_json,
options: SwaggerUiOptions::default(),
})
}
pub fn with_options(
ui_path: &str,
openapi: OpenApi,
options: SwaggerUiOptions,
) -> Result<Self> {
let api_doc_path = format!("{}/openapi.json", ui_path.trim_end_matches('/'));
let openapi_json = serde_json::to_string_pretty(&openapi).map_err(OpenApiError::Json)?;
Ok(Self {
ui_path: ui_path.to_string(),
api_doc_path,
openapi_json,
options,
})
}
fn matches_swagger_path(&self, path: &str) -> bool {
path == self.ui_path
|| path.starts_with(&format!("{}/", self.ui_path))
|| path == self.api_doc_path
}
async fn handle_swagger_request(&self, path: &str) -> Result<Response> {
if path == self.api_doc_path {
self.handle_openapi_json().await
} else if path == self.ui_path {
self.handle_ui_redirect().await
} else {
self.handle_ui_resource(path).await
}
}
async fn handle_openapi_json(&self) -> Result<Response> {
let mut response = Response::empty();
response.set_status(StatusCode::OK);
response.set_header(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("application/json; charset=utf-8"),
);
response.set_header(
http::header::ACCESS_CONTROL_ALLOW_ORIGIN,
http::HeaderValue::from_static("*"),
);
response.set_body(self.openapi_json.clone().into());
Ok(response)
}
async fn handle_ui_redirect(&self) -> Result<Response> {
let redirect_url = format!("{}/", self.ui_path);
let mut response = Response::empty();
response.set_status(StatusCode::MOVED_PERMANENTLY);
response.set_header(
http::header::LOCATION,
http::HeaderValue::from_str(&redirect_url)
.unwrap_or_else(|_| http::HeaderValue::from_static("/")),
);
Ok(response)
}
async fn handle_ui_resource(&self, path: &str) -> Result<Response> {
let relative_path = path
.strip_prefix(&format!("{}/", self.ui_path))
.unwrap_or("");
if relative_path.is_empty() || relative_path == "index.html" {
self.serve_swagger_ui_index().await
} else {
crate::ui_html::serve_asset(relative_path)
}
}
async fn serve_swagger_ui_index(&self) -> Result<Response> {
let html =
crate::ui_html::generate_index_html(&self.ui_path, &self.api_doc_path, &self.options);
let mut response = Response::empty();
response.set_status(StatusCode::OK);
response.set_header(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static("text/html; charset=utf-8"),
);
response.set_header(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("no-cache, no-store, must-revalidate"),
);
response.set_body(html.into());
Ok(response)
}
}
#[async_trait]
impl MiddleWareHandler for SwaggerUiMiddleware {
async fn handle(&self, req: Request, next: &Next) -> silent::Result<Response> {
let path = req.uri().path();
if self.matches_swagger_path(path) {
match self.handle_swagger_request(path).await {
Ok(response) => Ok(response),
Err(e) => {
eprintln!("Swagger UI中间件处理错误: {}", e);
let mut response = Response::empty();
response.set_status(StatusCode::INTERNAL_SERVER_ERROR);
response.set_body(format!("Swagger UI Error: {}", e).into());
Ok(response)
}
}
} else {
next.call(req).await
}
}
}
pub fn add_swagger_ui(
route: silent::prelude::Route,
ui_path: &str,
openapi: OpenApi,
) -> silent::prelude::Route {
match SwaggerUiMiddleware::new(ui_path, openapi) {
Ok(middleware) => route.hook(middleware),
Err(e) => {
eprintln!("创建Swagger UI中间件失败: {}", e);
route
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
info(title = "Test API", version = "1.0.0"),
paths(),
components(schemas())
)]
struct TestApiDoc;
#[test]
fn test_middleware_creation() {
let middleware = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi());
assert!(middleware.is_ok());
let middleware = middleware.unwrap();
assert_eq!(middleware.ui_path, "/docs");
assert_eq!(middleware.api_doc_path, "/docs/openapi.json");
}
#[test]
fn test_path_matching() {
let middleware = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
assert!(middleware.matches_swagger_path("/docs"));
assert!(middleware.matches_swagger_path("/docs/"));
assert!(middleware.matches_swagger_path("/docs/index.html"));
assert!(middleware.matches_swagger_path("/docs/openapi.json"));
assert!(!middleware.matches_swagger_path("/api/users"));
assert!(!middleware.matches_swagger_path("/doc"));
}
#[tokio::test]
async fn test_openapi_json_handling() {
let middleware = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
let response = middleware.handle_openapi_json().await.unwrap();
let content_type = response.headers().get(http::header::CONTENT_TYPE);
assert!(content_type.is_some());
assert!(
response
.headers()
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
.is_some()
);
}
#[tokio::test]
async fn test_redirect_on_base_path() {
let middleware = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
let resp = middleware.handle_swagger_request("/docs").await.unwrap();
assert!(resp.headers().get(http::header::LOCATION).is_some());
}
#[tokio::test]
async fn test_custom_api_doc_path() {
let mw = SwaggerUiMiddleware::with_custom_api_doc_path(
"/docs",
"/openapi-docs.json",
TestApiDoc::openapi(),
)
.unwrap();
assert!(mw.matches_swagger_path("/openapi-docs.json"));
let resp = mw
.handle_swagger_request("/openapi-docs.json")
.await
.unwrap();
assert!(
resp.headers()
.get(http::header::CONTENT_TYPE)
.map(|v| v.to_str().unwrap_or("").contains("application/json"))
.unwrap_or(false)
);
}
#[tokio::test]
async fn test_non_match_request_path() {
let mw = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
assert!(!mw.matches_swagger_path("/other"));
}
#[tokio::test]
async fn test_asset_404_branch() {
let mw = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
let resp = mw.handle_swagger_request("/docs/app.css").await.unwrap();
assert!(resp.headers().get(http::header::LOCATION).is_none());
}
#[tokio::test]
async fn test_index_html_headers() {
let mw = SwaggerUiMiddleware::new("/docs", TestApiDoc::openapi()).unwrap();
let resp = mw.handle_swagger_request("/docs/index.html").await.unwrap();
let ct = resp.headers().get(http::header::CONTENT_TYPE).unwrap();
assert!(ct.to_str().unwrap_or("").contains("text/html"));
assert!(resp.headers().get(http::header::CACHE_CONTROL).is_some());
}
}