Skip to main content

sz_rust_core/
openapi.rs

1//! OpenAPI 文档 — 对齐 Swagger / OpenAPI 3.0.3
2//!
3//! 提供编程式 OpenAPI 规范构建器,支持生成 JSON/YAML 规范文件并提供
4//! Swagger UI 渲染端点。无需 derive 宏,业务代码通过链式 API 注册端点。
5//!
6//! ## 使用示例
7//!
8//! ```ignore
9//! use sz_rust_core::openapi::{OpenApiBuilder, HttpMethod};
10//!
11//! let spec = OpenApiBuilder::new("SZ-Rust API", "1.0.0")
12//!     .description("SZ-Rust 框架 API 文档")
13//!     .path("/api/v1/users", HttpMethod::Get, |op| {
14//!         op.summary("获取用户列表")
15//!           .tag("用户")
16//!           .response(200, "成功", "application/json")
17//!     })
18//!     .path("/api/v1/users/{id}", HttpMethod::Get, |op| {
19//!         op.summary("获取用户详情")
20//!           .tag("用户")
21//!           .parameter("id", "path", "用户 ID", true, "integer")
22//!           .response(200, "成功", "application/json")
23//!           .response(404, "用户不存在", "application/json")
24//!     })
25//!     .build();
26//!
27//! // spec 为 serde_json::Value,可直接序列化为 JSON
28//! let json = serde_json::to_string_pretty(&spec).unwrap();
29//! ```
30//!
31//! ## Swagger UI 集成
32//!
33//! 通过 [`swagger_ui_html`] 获取 Swagger UI HTML 页面,挂载到 axum 路由:
34//!
35//! ```ignore
36//! use sz_rust_core::openapi::{OpenApiBuilder, swagger_ui_html};
37//!
38//! let spec_json = serde_json::to_string(&builder.build()).unwrap();
39//! let html = swagger_ui_html(&spec_json);
40//! ```
41
42use serde_json::{json, Map, Value};
43
44/// HTTP 方法枚举
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum HttpMethod {
47    /// HTTP GET
48    Get,
49    /// HTTP POST
50    Post,
51    /// HTTP PUT
52    Put,
53    /// HTTP DELETE
54    Delete,
55    /// HTTP PATCH
56    Patch,
57    /// HTTP OPTIONS
58    Options,
59    /// HTTP HEAD
60    Head,
61}
62
63impl HttpMethod {
64    /// 转为 OpenAPI 规范的小写字符串
65    pub fn as_str(&self) -> &'static str {
66        match self {
67            HttpMethod::Get => "get",
68            HttpMethod::Post => "post",
69            HttpMethod::Put => "put",
70            HttpMethod::Delete => "delete",
71            HttpMethod::Patch => "patch",
72            HttpMethod::Options => "options",
73            HttpMethod::Head => "head",
74        }
75    }
76}
77
78/// OpenAPI 规范构建器
79///
80/// 对齐 OpenAPI 3.0.3 规范,通过链式 API 构建 spec。
81pub struct OpenApiBuilder {
82    title: String,
83    version: String,
84    description: Option<String>,
85    paths: Map<String, Value>,
86    tags: Vec<Value>,
87    security_schemes: Map<String, Value>,
88}
89
90impl OpenApiBuilder {
91    /// 创建新的构建器
92    ///
93    /// # 参数
94    ///
95    /// - `title`:API 标题
96    /// - `version`:API 版本
97    pub fn new(title: &str, version: &str) -> Self {
98        Self {
99            title: title.to_string(),
100            version: version.to_string(),
101            description: None,
102            paths: Map::new(),
103            tags: Vec::new(),
104            security_schemes: Map::new(),
105        }
106    }
107
108    /// 设置 API 描述
109    pub fn description(mut self, desc: &str) -> Self {
110        self.description = Some(desc.to_string());
111        self
112    }
113
114    /// 添加标签(用于分组)
115    pub fn tag(mut self, name: &str, description: &str) -> Self {
116        self.tags.push(json!({
117            "name": name,
118            "description": description,
119        }));
120        self
121    }
122
123    /// 添加 API 端点
124    ///
125    /// # 参数
126    ///
127    /// - `path`:路径(如 `/api/v1/users/{id}`)
128    /// - `method`:HTTP 方法
129    /// - `config`:操作配置闭包
130    pub fn path<F>(mut self, path: &str, method: HttpMethod, config: F) -> Self
131    where
132        F: FnOnce(&mut OperationBuilder),
133    {
134        let mut op = OperationBuilder::new();
135        config(&mut op);
136
137        let path_entry = self
138            .paths
139            .entry(path.to_string())
140            .or_insert_with(|| json!({}));
141        if let Value::Object(ref mut obj) = path_entry {
142            obj.insert(method.as_str().to_string(), op.build());
143        }
144        self
145    }
146
147    /// 添加 Bearer Token 安全方案
148    pub fn bearer_auth(mut self, scheme_name: &str) -> Self {
149        self.security_schemes.insert(
150            scheme_name.to_string(),
151            json!({
152                "type": "http",
153                "scheme": "bearer",
154                "bearerFormat": "JWT",
155            }),
156        );
157        self
158    }
159
160    /// 添加 API Key 安全方案
161    pub fn api_key_auth(mut self, scheme_name: &str, header_name: &str) -> Self {
162        self.security_schemes.insert(
163            scheme_name.to_string(),
164            json!({
165                "type": "apiKey",
166                "in": "header",
167                "name": header_name,
168            }),
169        );
170        self
171    }
172
173    /// 构建 OpenAPI spec(`serde_json::Value`)
174    pub fn build(self) -> Value {
175        let mut info = json!({
176            "title": self.title,
177            "version": self.version,
178        });
179        if let Some(desc) = self.description {
180            info["description"] = json!(desc);
181        }
182
183        let mut spec = json!({
184            "openapi": "3.0.3",
185            "info": info,
186            "paths": self.paths,
187        });
188
189        let mut components = Map::new();
190        if !self.security_schemes.is_empty() {
191            components.insert(
192                "securitySchemes".to_string(),
193                Value::Object(self.security_schemes),
194            );
195        }
196        if !components.is_empty() {
197            spec["components"] = Value::Object(components);
198        }
199
200        if !self.tags.is_empty() {
201            spec["tags"] = Value::Array(self.tags);
202        }
203
204        spec
205    }
206
207    /// 构建 JSON 字符串(美化格式)
208    pub fn to_json_string(self) -> String {
209        serde_json::to_string_pretty(&self.build()).unwrap_or_else(|_| "{}".to_string())
210    }
211}
212
213/// 操作构建器 — 描述单个 API 端点的元数据
214pub struct OperationBuilder {
215    summary: Option<String>,
216    description: Option<String>,
217    tags: Vec<String>,
218    parameters: Vec<Value>,
219    responses: Map<String, Value>,
220    deprecated: bool,
221}
222
223impl OperationBuilder {
224    /// 创建新的操作构建器
225    pub fn new() -> Self {
226        Self {
227            summary: None,
228            description: None,
229            tags: Vec::new(),
230            parameters: Vec::new(),
231            responses: Map::new(),
232            deprecated: false,
233        }
234    }
235
236    /// 设置摘要
237    pub fn summary(&mut self, summary: &str) -> &mut Self {
238        self.summary = Some(summary.to_string());
239        self
240    }
241
242    /// 设置详细描述
243    pub fn description(&mut self, desc: &str) -> &mut Self {
244        self.description = Some(desc.to_string());
245        self
246    }
247
248    /// 添加标签(用于分组)
249    pub fn tag(&mut self, tag: &str) -> &mut Self {
250        self.tags.push(tag.to_string());
251        self
252    }
253
254    /// 添加参数
255    ///
256    /// # 参数
257    ///
258    /// - `name`:参数名
259    /// - `location`:参数位置(`path` / `query` / `header` / `cookie`)
260    /// - `desc`:参数描述
261    /// - `required`:是否必填
262    /// - `schema_type`:数据类型(`string` / `integer` / `number` / `boolean` / `array`)
263    pub fn parameter(
264        &mut self,
265        name: &str,
266        location: &str,
267        desc: &str,
268        required: bool,
269        schema_type: &str,
270    ) -> &mut Self {
271        self.parameters.push(json!({
272            "name": name,
273            "in": location,
274            "description": desc,
275            "required": required,
276            "schema": {
277                "type": schema_type
278            }
279        }));
280        self
281    }
282
283    /// 添加响应
284    ///
285    /// # 参数
286    ///
287    /// - `status`:HTTP 状态码(如 `200`、`404`)
288    /// - `desc`:响应描述
289    /// - `content_type`:内容类型(如 `"application/json"`)
290    pub fn response(&mut self, status: u16, desc: &str, content_type: &str) -> &mut Self {
291        self.responses.insert(
292            status.to_string(),
293            json!({
294                "description": desc,
295                "content": {
296                    content_type: {
297                        "schema": {
298                            "type": "object"
299                        }
300                    }
301                }
302            }),
303        );
304        self
305    }
306
307    /// 添加响应(带 schema 引用)
308    ///
309    /// # 参数
310    ///
311    /// - `status`:HTTP 状态码
312    /// - `desc`:响应描述
313    /// - `content_type`:内容类型
314    /// - `schema_ref`:schema 引用名(如 `"#/components/schemas/User"`)
315    pub fn response_with_schema(
316        &mut self,
317        status: u16,
318        desc: &str,
319        content_type: &str,
320        schema_ref: &str,
321    ) -> &mut Self {
322        self.responses.insert(
323            status.to_string(),
324            json!({
325                "description": desc,
326                "content": {
327                    content_type: {
328                        "schema": {
329                            "$ref": schema_ref
330                        }
331                    }
332                }
333            }),
334        );
335        self
336    }
337
338    /// 标记为已弃用
339    pub fn deprecated(&mut self) -> &mut Self {
340        self.deprecated = true;
341        self
342    }
343
344    /// 构建操作 JSON
345    fn build(self) -> Value {
346        let mut op = Map::new();
347        if let Some(s) = self.summary {
348            op.insert("summary".to_string(), json!(s));
349        }
350        if let Some(d) = self.description {
351            op.insert("description".to_string(), json!(d));
352        }
353        if !self.tags.is_empty() {
354            op.insert("tags".to_string(), json!(self.tags));
355        }
356        if !self.parameters.is_empty() {
357            op.insert("parameters".to_string(), json!(self.parameters));
358        }
359        if !self.responses.is_empty() {
360            op.insert("responses".to_string(), Value::Object(self.responses));
361        } else {
362            // 默认响应
363            op.insert(
364                "responses".to_string(),
365                json!({
366                    "200": {
367                        "description": "成功"
368                    }
369                }),
370            );
371        }
372        if self.deprecated {
373            op.insert("deprecated".to_string(), json!(true));
374        }
375        Value::Object(op)
376    }
377}
378
379impl Default for OperationBuilder {
380    fn default() -> Self {
381        Self::new()
382    }
383}
384
385/// 生成 Swagger UI HTML 页面
386///
387/// 通过 CDN 加载 Swagger UI,将 OpenAPI JSON 内嵌到页面中。
388///
389/// # 参数
390///
391/// - `spec_json`:OpenAPI 规范 JSON 字符串
392pub fn swagger_ui_html(spec_json: &str) -> String {
393    format!(
394        r#"<!DOCTYPE html>
395<html lang="zh-CN">
396<head>
397    <meta charset="UTF-8">
398    <meta name="viewport" content="width=device-width, initial-scale=1.0">
399    <title>API 文档 - Swagger UI</title>
400    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
401    <style>
402        body {{ margin: 0; }}
403    </style>
404</head>
405<body>
406    <div id="swagger-ui"></div>
407    <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
408    <script>
409        window.onload = function() {{
410            const spec = {spec_json};
411            SwaggerUIBundle({{
412                spec: spec,
413                dom_id: '#swagger-ui',
414                presets: [SwaggerUIBundle.presets.apis],
415                layout: 'BaseLayout',
416            }});
417        }};
418    </script>
419</body>
420</html>"#
421    )
422}
423
424/// 生成 Redoc HTML 页面(替代 Swagger UI 的轻量文档查看器)
425///
426/// # 参数
427///
428/// - `spec_json`:OpenAPI 规范 JSON 字符串
429pub fn redoc_html(spec_json: &str) -> String {
430    format!(
431        r#"<!DOCTYPE html>
432<html lang="zh-CN">
433<head>
434    <meta charset="UTF-8">
435    <meta name="viewport" content="width=device-width, initial-scale=1.0">
436    <title>API 文档 - Redoc</title>
437    <style>
438        body {{ margin: 0; padding: 0; }}
439    </style>
440</head>
441<body>
442    <redoc spec-json='{spec_json}'></redoc>
443    <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
444</body>
445</html>"#
446    )
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_http_method_as_str() {
455        assert_eq!(HttpMethod::Get.as_str(), "get");
456        assert_eq!(HttpMethod::Post.as_str(), "post");
457        assert_eq!(HttpMethod::Put.as_str(), "put");
458        assert_eq!(HttpMethod::Delete.as_str(), "delete");
459        assert_eq!(HttpMethod::Patch.as_str(), "patch");
460        assert_eq!(HttpMethod::Options.as_str(), "options");
461        assert_eq!(HttpMethod::Head.as_str(), "head");
462    }
463
464    #[test]
465    fn test_openapi_builder_basic() {
466        let spec = OpenApiBuilder::new("Test API", "1.0.0")
467            .description("A test API")
468            .build();
469
470        assert_eq!(spec["openapi"], "3.0.3");
471        assert_eq!(spec["info"]["title"], "Test API");
472        assert_eq!(spec["info"]["version"], "1.0.0");
473        assert_eq!(spec["info"]["description"], "A test API");
474        assert!(spec["paths"].is_object());
475    }
476
477    #[test]
478    fn test_openapi_builder_with_path() {
479        let spec = OpenApiBuilder::new("Test API", "1.0.0")
480            .path("/api/v1/users", HttpMethod::Get, |op| {
481                op.summary("获取用户列表")
482                    .tag("用户")
483                    .response(200, "成功", "application/json");
484            })
485            .build();
486
487        let path = &spec["paths"]["/api/v1/users"]["get"];
488        assert_eq!(path["summary"], "获取用户列表");
489        assert_eq!(path["tags"][0], "用户");
490        assert_eq!(path["responses"]["200"]["description"], "成功");
491    }
492
493    #[test]
494    fn test_openapi_builder_with_parameter() {
495        let spec = OpenApiBuilder::new("Test API", "1.0.0")
496            .path("/api/v1/users/{id}", HttpMethod::Get, |op| {
497                op.summary("获取用户详情")
498                    .parameter("id", "path", "用户 ID", true, "integer")
499                    .response(200, "成功", "application/json")
500                    .response(404, "用户不存在", "application/json");
501            })
502            .build();
503
504        let path = &spec["paths"]["/api/v1/users/{id}"]["get"];
505        assert_eq!(path["parameters"][0]["name"], "id");
506        assert_eq!(path["parameters"][0]["in"], "path");
507        assert_eq!(path["parameters"][0]["required"], true);
508        assert_eq!(path["parameters"][0]["schema"]["type"], "integer");
509        assert_eq!(path["responses"]["404"]["description"], "用户不存在");
510    }
511
512    #[test]
513    fn test_openapi_builder_with_tags() {
514        let spec = OpenApiBuilder::new("Test API", "1.0.0")
515            .tag("用户", "用户管理接口")
516            .tag("订单", "订单管理接口")
517            .build();
518
519        assert_eq!(spec["tags"][0]["name"], "用户");
520        assert_eq!(spec["tags"][0]["description"], "用户管理接口");
521        assert_eq!(spec["tags"][1]["name"], "订单");
522    }
523
524    #[test]
525    fn test_openapi_builder_bearer_auth() {
526        let spec = OpenApiBuilder::new("Test API", "1.0.0")
527            .bearer_auth("BearerAuth")
528            .build();
529
530        let scheme = &spec["components"]["securitySchemes"]["BearerAuth"];
531        assert_eq!(scheme["type"], "http");
532        assert_eq!(scheme["scheme"], "bearer");
533        assert_eq!(scheme["bearerFormat"], "JWT");
534    }
535
536    #[test]
537    fn test_openapi_builder_api_key_auth() {
538        let spec = OpenApiBuilder::new("Test API", "1.0.0")
539            .api_key_auth("ApiKeyAuth", "X-API-Key")
540            .build();
541
542        let scheme = &spec["components"]["securitySchemes"]["ApiKeyAuth"];
543        assert_eq!(scheme["type"], "apiKey");
544        assert_eq!(scheme["in"], "header");
545        assert_eq!(scheme["name"], "X-API-Key");
546    }
547
548    #[test]
549    fn test_openapi_builder_default_response() {
550        let spec = OpenApiBuilder::new("Test API", "1.0.0")
551            .path("/api/v1/health", HttpMethod::Get, |op| {
552                op.summary("健康检查");
553            })
554            .build();
555
556        let path = &spec["paths"]["/api/v1/health"]["get"];
557        // 未指定响应时应有默认 200 响应
558        assert_eq!(path["responses"]["200"]["description"], "成功");
559    }
560
561    #[test]
562    fn test_openapi_builder_deprecated() {
563        let spec = OpenApiBuilder::new("Test API", "1.0.0")
564            .path("/api/v1/old", HttpMethod::Get, |op| {
565                op.summary("旧接口").deprecated();
566            })
567            .build();
568
569        let path = &spec["paths"]["/api/v1/old"]["get"];
570        assert_eq!(path["deprecated"], true);
571    }
572
573    #[test]
574    fn test_openapi_builder_multiple_methods_same_path() {
575        let spec = OpenApiBuilder::new("Test API", "1.0.0")
576            .path("/api/v1/users", HttpMethod::Get, |op| {
577                op.summary("获取列表");
578            })
579            .path("/api/v1/users", HttpMethod::Post, |op| {
580                op.summary("创建用户");
581            })
582            .build();
583
584        let path = &spec["paths"]["/api/v1/users"];
585        assert!(path["get"].is_object());
586        assert!(path["post"].is_object());
587        assert_eq!(path["get"]["summary"], "获取列表");
588        assert_eq!(path["post"]["summary"], "创建用户");
589    }
590
591    #[test]
592    fn test_openapi_builder_to_json_string() {
593        let builder = OpenApiBuilder::new("Test API", "1.0.0");
594        let json = builder.to_json_string();
595        assert!(json.contains("\"openapi\": \"3.0.3\""));
596        assert!(json.contains("\"title\": \"Test API\""));
597        assert!(json.contains("\"version\": \"1.0.0\""));
598    }
599
600    #[test]
601    fn test_openapi_builder_response_with_schema() {
602        let spec = OpenApiBuilder::new("Test API", "1.0.0")
603            .path("/api/v1/users/{id}", HttpMethod::Get, |op| {
604                op.response_with_schema(
605                    200,
606                    "成功",
607                    "application/json",
608                    "#/components/schemas/User",
609                );
610            })
611            .build();
612
613        let schema_ref = &spec["paths"]["/api/v1/users/{id}"]["get"]["responses"]["200"]["content"]
614            ["application/json"]["schema"]["$ref"];
615        assert_eq!(schema_ref, "#/components/schemas/User");
616    }
617
618    #[test]
619    fn test_swagger_ui_html_contains_spec() {
620        let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
621        let html = swagger_ui_html(spec_json);
622        assert!(html.contains("<!DOCTYPE html>"));
623        assert!(html.contains("swagger-ui"));
624        assert!(html.contains(spec_json));
625    }
626
627    #[test]
628    fn test_redoc_html_contains_spec() {
629        let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
630        let html = redoc_html(spec_json);
631        assert!(html.contains("<!DOCTYPE html>"));
632        assert!(html.contains("redoc"));
633        assert!(html.contains(spec_json));
634    }
635
636    #[test]
637    fn test_operation_builder_default() {
638        let op = OperationBuilder::default();
639        assert!(op.summary.is_none());
640        assert!(op.description.is_none());
641        assert!(op.tags.is_empty());
642        assert!(op.parameters.is_empty());
643        assert!(op.responses.is_empty());
644        assert!(!op.deprecated);
645    }
646
647    #[test]
648    fn test_openapi_builder_no_description() {
649        let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
650        assert!(spec["info"]["description"].is_null());
651    }
652
653    #[test]
654    fn test_openapi_builder_no_security_schemes() {
655        let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
656        // 无安全方案时不应有 components
657        assert!(spec.get("components").is_none() || spec["components"].is_null());
658    }
659
660    #[test]
661    fn test_openapi_builder_full_spec() {
662        let spec = OpenApiBuilder::new("SZ-Rust API", "1.0.0")
663            .description("全栈 API 文档")
664            .tag("用户", "用户管理")
665            .tag("认证", "认证授权")
666            .bearer_auth("BearerAuth")
667            .path("/api/v1/auth/login", HttpMethod::Post, |op| {
668                op.summary("用户登录")
669                    .description("通过用户名密码获取 JWT Token")
670                    .tag("认证")
671                    .parameter("username", "query", "用户名", true, "string")
672                    .parameter("password", "query", "密码", true, "string")
673                    .response(200, "登录成功", "application/json")
674                    .response(401, "认证失败", "application/json");
675            })
676            .path("/api/v1/users", HttpMethod::Get, |op| {
677                op.summary("获取用户列表").tag("用户").response_with_schema(
678                    200,
679                    "成功",
680                    "application/json",
681                    "#/components/schemas/UserList",
682                );
683            })
684            .path("/api/v1/users/{id}", HttpMethod::Delete, |op| {
685                op.summary("删除用户")
686                    .tag("用户")
687                    .parameter("id", "path", "用户 ID", true, "integer")
688                    .response(204, "删除成功", "application/json")
689                    .response(404, "用户不存在", "application/json");
690            })
691            .build();
692
693        // 验证基本结构
694        assert_eq!(spec["openapi"], "3.0.3");
695        assert_eq!(spec["info"]["title"], "SZ-Rust API");
696        assert_eq!(spec["info"]["description"], "全栈 API 文档");
697
698        // 验证路径
699        assert_eq!(spec["paths"].as_object().unwrap().len(), 3);
700
701        // 验证标签
702        assert_eq!(spec["tags"].as_array().unwrap().len(), 2);
703
704        // 验证安全方案
705        assert_eq!(
706            spec["components"]["securitySchemes"]["BearerAuth"]["scheme"],
707            "bearer"
708        );
709
710        // 验证 login 端点
711        let login = &spec["paths"]["/api/v1/auth/login"]["post"];
712        assert_eq!(login["summary"], "用户登录");
713        assert_eq!(login["description"], "通过用户名密码获取 JWT Token");
714        assert_eq!(login["tags"][0], "认证");
715        assert_eq!(login["parameters"].as_array().unwrap().len(), 2);
716        assert_eq!(login["responses"]["401"]["description"], "认证失败");
717
718        // 验证 delete 端点
719        let delete = &spec["paths"]["/api/v1/users/{id}"]["delete"];
720        assert_eq!(delete["summary"], "删除用户");
721        assert_eq!(delete["responses"]["204"]["description"], "删除成功");
722    }
723}