Skip to main content

silent_openapi/
ui_html.rs

1//! Swagger UI HTML 生成与静态资源服务
2//!
3//! 统一管理 Handler 和 Middleware 共用的 HTML 模板与资源分发逻辑。
4//! 当启用 `swagger-ui-embedded` feature 时,静态资源从编译时嵌入的二进制数据中读取;
5//! 否则从 unpkg CDN 加载。
6
7use crate::{Result, SwaggerUiOptions};
8use silent::{Response, StatusCode};
9
10/// CDN 版本号(与 build.rs 中保持一致)
11const SWAGGER_UI_VERSION: &str = "5.17.14";
12
13/// 生成 Swagger UI 主页 HTML
14///
15/// 根据是否启用 `swagger-ui-embedded` feature,
16/// 自动选择从本地相对路径或 CDN 加载静态资源。
17pub fn generate_index_html(
18    ui_path: &str,
19    api_doc_path: &str,
20    options: &SwaggerUiOptions,
21) -> String {
22    let ui_base = ui_path.trim_end_matches('/');
23
24    let (css_href, favicon_href, bundle_src, preset_src) = if cfg!(feature = "swagger-ui-embedded")
25    {
26        (
27            format!("{ui_base}/swagger-ui.css"),
28            format!("{ui_base}/favicon-32x32.png"),
29            format!("{ui_base}/swagger-ui-bundle.js"),
30            format!("{ui_base}/swagger-ui-standalone-preset.js"),
31        )
32    } else {
33        let cdn = format!("https://unpkg.com/swagger-ui-dist@{SWAGGER_UI_VERSION}");
34        (
35            format!("{cdn}/swagger-ui.css"),
36            format!("{cdn}/favicon-32x32.png"),
37            format!("{cdn}/swagger-ui-bundle.js"),
38            format!("{cdn}/swagger-ui-standalone-preset.js"),
39        )
40    };
41
42    let try_it_out = if options.try_it_out_enabled {
43        "true"
44    } else {
45        "false"
46    };
47
48    format!(
49        r#"<!DOCTYPE html>
50<html lang="zh-CN">
51<head>
52    <meta charset="UTF-8">
53    <meta name="viewport" content="width=device-width, initial-scale=1.0">
54    <title>API Documentation - Swagger UI</title>
55    <link rel="stylesheet" type="text/css" href="{css_href}" />
56    <link rel="icon" type="image/png" href="{favicon_href}" sizes="32x32" />
57    <style>
58        html {{
59            box-sizing: border-box;
60            overflow: -moz-scrollbars-vertical;
61            overflow-y: scroll;
62        }}
63        *, *:before, *:after {{
64            box-sizing: inherit;
65        }}
66        body {{
67            margin: 0;
68            background: #fafafa;
69        }}
70        .swagger-ui .topbar {{
71            display: none;
72        }}
73        .swagger-ui .info {{
74            margin: 50px 0;
75        }}
76        .custom-header {{
77            background: #89CFF0;
78            padding: 20px;
79            text-align: center;
80            color: #1976d2;
81            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
82        }}
83        .custom-header h1 {{
84            margin: 0;
85            font-size: 24px;
86            font-weight: 600;
87        }}
88        .custom-header p {{
89            margin: 8px 0 0 0;
90            opacity: 0.8;
91        }}
92    </style>
93</head>
94<body>
95    <div class="custom-header">
96        <h1>Silent Framework API Documentation</h1>
97        <p>OpenAPI 3.0</p>
98    </div>
99    <div id="swagger-ui"></div>
100
101    <script src="{bundle_src}"></script>
102    <script src="{preset_src}"></script>
103    <script>
104        window.onload = function() {{
105            const ui = SwaggerUIBundle({{
106                url: '{api_doc_path}',
107                dom_id: '#swagger-ui',
108                deepLinking: true,
109                presets: [
110                    SwaggerUIBundle.presets.apis,
111                    SwaggerUIStandalonePreset
112                ],
113                plugins: [
114                    SwaggerUIBundle.plugins.DownloadUrl
115                ],
116                layout: "StandaloneLayout",
117                validatorUrl: null,
118                docExpansion: "list",
119                defaultModelsExpandDepth: 1,
120                defaultModelExpandDepth: 1,
121                displayRequestDuration: true,
122                filter: true,
123                showExtensions: true,
124                showCommonExtensions: true,
125                tryItOutEnabled: {try_it_out}
126            }});
127            window.ui = ui;
128        }}
129    </script>
130</body>
131</html>"#
132    )
133}
134
135/// 服务 Swagger UI 静态资源
136///
137/// 当 `swagger-ui-embedded` feature 启用时,从嵌入的二进制数据中返回资源;
138/// 否则返回 404(由 CDN 直接在浏览器端加载)。
139pub fn serve_asset(#[allow(unused_variables)] asset_path: &str) -> Result<Response> {
140    #[cfg(feature = "swagger-ui-embedded")]
141    {
142        if let Some((content_type, data)) = crate::embedded::get_embedded_asset(asset_path) {
143            let mut response = Response::empty();
144            response.set_status(StatusCode::OK);
145            response.set_header(
146                http::header::CONTENT_TYPE,
147                http::HeaderValue::from_static(content_type),
148            );
149            response.set_header(
150                http::header::CACHE_CONTROL,
151                http::HeaderValue::from_static("public, max-age=86400"),
152            );
153            response.set_body(data.to_vec().into());
154            return Ok(response);
155        }
156    }
157
158    let mut response = Response::empty();
159    response.set_status(StatusCode::NOT_FOUND);
160    response.set_body("Asset not found".into());
161    Ok(response)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn test_generate_index_html_contains_api_doc_path() {
170        let html = generate_index_html("/docs", "/docs/openapi.json", &SwaggerUiOptions::default());
171        assert!(html.contains("/docs/openapi.json"));
172        assert!(html.contains("swagger-ui"));
173    }
174
175    #[test]
176    fn test_generate_index_html_try_it_out_disabled() {
177        let options = SwaggerUiOptions {
178            try_it_out_enabled: false,
179        };
180        let html = generate_index_html("/docs", "/docs/openapi.json", &options);
181        assert!(html.contains("tryItOutEnabled: false"));
182    }
183
184    #[test]
185    fn test_generate_index_html_resource_source() {
186        let html = generate_index_html("/docs", "/docs/openapi.json", &SwaggerUiOptions::default());
187
188        if cfg!(feature = "swagger-ui-embedded") {
189            // 嵌入模式:使用相对路径
190            assert!(html.contains("/docs/swagger-ui-bundle.js"));
191            assert!(html.contains("/docs/swagger-ui.css"));
192        } else {
193            // CDN 模式:使用 unpkg
194            assert!(html.contains("unpkg.com/swagger-ui-dist@"));
195        }
196    }
197
198    #[test]
199    fn test_serve_asset_not_found() {
200        // 不存在的资源应返回成功(404 响应体)
201        let resp = serve_asset("nonexistent.file");
202        assert!(resp.is_ok());
203    }
204}