Skip to main content

sz_rust_router_facade/
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_router_facade::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_router_facade::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/// 从路由配置自动扫描生成 OpenAPI 路径
45///
46/// 将 [`crate::routing::RouteConfig`] 中全部路由规则(含分组展平后的规则)
47/// 自动转换为 OpenAPI 操作,无需手工逐条注册。
48///
49/// ## 自动生成规则
50///
51/// - `summary`:取 handler 引用(如 `User@list`)
52/// - `tag`:取路径第一段(如 `/api/v1/users` → `users`),用于分组展示
53/// - `parameters`:自动识别路径中的 `{param}` 模板变量,生成必填 path 参数
54/// - `responses`:默认注册 200(成功)与 404(资源不存在)
55pub fn routes_to_spec(routes: &[crate::routing::RouteRule], spec: &mut Value) {
56    // 先收集待追加的 tag(路径首段去重),避免对 spec 的双重可变借用
57    let mut tags_to_add: Vec<(String, String)> = Vec::new();
58    let mut seen: std::collections::HashSet<String> = spec
59        .get("tags")
60        .and_then(|t| t.as_array())
61        .map(|arr| {
62            arr.iter()
63                .filter_map(|t| t.get("name").and_then(|n| n.as_str()))
64                .map(|s| s.to_string())
65                .collect()
66        })
67        .unwrap_or_default();
68
69    for rule in routes {
70        let tag = rule
71            .path
72            .trim_start_matches('/')
73            .split('/')
74            .next()
75            .unwrap_or("default")
76            .to_string();
77        if seen.insert(tag.clone()) {
78            tags_to_add.push((tag.clone(), format!("{} 相关接口", tag)));
79        }
80    }
81
82    // 路径生成(单一可变借用区间)
83    {
84        let paths = spec
85            .get_mut("paths")
86            .and_then(|p| p.as_object_mut())
87            .expect("spec must contain a paths object");
88
89        for rule in routes {
90            let method = match rule.method {
91                crate::routing::HttpMethod::GET => "get",
92                crate::routing::HttpMethod::POST => "post",
93                crate::routing::HttpMethod::PUT => "put",
94                crate::routing::HttpMethod::DELETE => "delete",
95                crate::routing::HttpMethod::PATCH => "patch",
96                crate::routing::HttpMethod::OPTIONS => "options",
97            };
98
99            // 从路径首段派生 tag(如 /api/v1/users → users)
100            let tag = rule
101                .path
102                .trim_start_matches('/')
103                .split('/')
104                .next()
105                .unwrap_or("default")
106                .to_string();
107
108            // 自动识别路径模板变量 → path 参数
109            let mut parameters = Vec::new();
110            for segment in rule.path.split('/') {
111                if let Some(name) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
112                    parameters.push(json!({
113                        "name": name,
114                        "in": "path",
115                        "required": true,
116                        "description": format!("路径参数 {}", name),
117                        "schema": { "type": "string" },
118                    }));
119                }
120            }
121
122            let mut op = json!({
123                "summary": rule.handler,
124                "tags": [tag],
125                "responses": {
126                    "200": { "description": "成功" },
127                    "404": { "description": "资源不存在" }
128                },
129            });
130            if !parameters.is_empty() {
131                op["parameters"] = Value::Array(parameters);
132            }
133            if !rule.middleware.is_empty() {
134                op["description"] = json!(format!("中间件: {}", rule.middleware.join(", ")));
135            }
136
137            let path_entry = paths.entry(rule.path.clone()).or_insert_with(|| json!({}));
138            if let Value::Object(ref mut obj) = path_entry {
139                obj.insert(method.to_string(), op);
140            }
141        }
142    }
143
144    // 追加收集到的 tag(与手工注册的 tag 合并;spec 无 tags 时创建)
145    if !tags_to_add.is_empty() {
146        if spec.get("tags").is_none() {
147            spec["tags"] = Value::Array(Vec::new());
148        }
149        let tag_arr = spec
150            .get_mut("tags")
151            .and_then(|t| t.as_array_mut())
152            .expect("tags must be an array after creation");
153        for (name, desc) in tags_to_add {
154            tag_arr.push(json!({ "name": name, "description": desc }));
155        }
156    }
157}
158
159/// HTTP 方法枚举
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
161pub enum HttpMethod {
162    /// HTTP GET
163    Get,
164    /// HTTP POST
165    Post,
166    /// HTTP PUT
167    Put,
168    /// HTTP DELETE
169    Delete,
170    /// HTTP PATCH
171    Patch,
172    /// HTTP OPTIONS
173    Options,
174    /// HTTP HEAD
175    Head,
176}
177
178impl HttpMethod {
179    /// 转为 OpenAPI 规范的小写字符串
180    pub fn as_str(&self) -> &'static str {
181        match self {
182            HttpMethod::Get => "get",
183            HttpMethod::Post => "post",
184            HttpMethod::Put => "put",
185            HttpMethod::Delete => "delete",
186            HttpMethod::Patch => "patch",
187            HttpMethod::Options => "options",
188            HttpMethod::Head => "head",
189        }
190    }
191}
192
193/// OpenAPI 规范构建器
194///
195/// 对齐 OpenAPI 3.0.3 规范,通过链式 API 构建 spec。
196pub struct OpenApiBuilder {
197    title: String,
198    version: String,
199    description: Option<String>,
200    paths: Map<String, Value>,
201    tags: Vec<Value>,
202    security_schemes: Map<String, Value>,
203}
204
205impl OpenApiBuilder {
206    /// 创建新的构建器
207    ///
208    /// # 参数
209    ///
210    /// - `title`:API 标题
211    /// - `version`:API 版本
212    pub fn new(title: &str, version: &str) -> Self {
213        Self {
214            title: title.to_string(),
215            version: version.to_string(),
216            description: None,
217            paths: Map::new(),
218            tags: Vec::new(),
219            security_schemes: Map::new(),
220        }
221    }
222
223    /// 设置 API 描述
224    pub fn description(mut self, desc: &str) -> Self {
225        self.description = Some(desc.to_string());
226        self
227    }
228
229    /// 添加标签(用于分组)
230    pub fn tag(mut self, name: &str, description: &str) -> Self {
231        self.tags.push(json!({
232            "name": name,
233            "description": description,
234        }));
235        self
236    }
237
238    /// 添加 API 端点
239    ///
240    /// # 参数
241    ///
242    /// - `path`:路径(如 `/api/v1/users/{id}`)
243    /// - `method`:HTTP 方法
244    /// - `config`:操作配置闭包
245    pub fn path<F>(mut self, path: &str, method: HttpMethod, config: F) -> Self
246    where
247        F: FnOnce(&mut OperationBuilder),
248    {
249        let mut op = OperationBuilder::new();
250        config(&mut op);
251
252        let path_entry = self
253            .paths
254            .entry(path.to_string())
255            .or_insert_with(|| json!({}));
256        if let Value::Object(ref mut obj) = path_entry {
257            obj.insert(method.as_str().to_string(), op.build());
258        }
259        self
260    }
261
262    /// 添加 Bearer Token 安全方案
263    pub fn bearer_auth(mut self, scheme_name: &str) -> Self {
264        self.security_schemes.insert(
265            scheme_name.to_string(),
266            json!({
267                "type": "http",
268                "scheme": "bearer",
269                "bearerFormat": "JWT",
270            }),
271        );
272        self
273    }
274
275    /// 添加 API Key 安全方案
276    pub fn api_key_auth(mut self, scheme_name: &str, header_name: &str) -> Self {
277        self.security_schemes.insert(
278            scheme_name.to_string(),
279            json!({
280                "type": "apiKey",
281                "in": "header",
282                "name": header_name,
283            }),
284        );
285        self
286    }
287
288    /// 构建 OpenAPI spec(`serde_json::Value`)
289    pub fn build(self) -> Value {
290        let mut info = json!({
291            "title": self.title,
292            "version": self.version,
293        });
294        if let Some(desc) = self.description {
295            info["description"] = json!(desc);
296        }
297
298        let mut spec = json!({
299            "openapi": "3.0.3",
300            "info": info,
301            "paths": self.paths,
302        });
303
304        let mut components = Map::new();
305        if !self.security_schemes.is_empty() {
306            components.insert(
307                "securitySchemes".to_string(),
308                Value::Object(self.security_schemes),
309            );
310        }
311        if !components.is_empty() {
312            spec["components"] = Value::Object(components);
313        }
314
315        if !self.tags.is_empty() {
316            spec["tags"] = Value::Array(self.tags);
317        }
318
319        spec
320    }
321
322    /// 构建 JSON 字符串(美化格式)
323    pub fn to_json_string(self) -> String {
324        serde_json::to_string_pretty(&self.build()).unwrap_or_else(|_| "{}".to_string())
325    }
326}
327
328/// 从路由配置自动扫描生成完整 OpenAPI spec
329///
330/// 扫描 [`crate::routing::RouteConfig`] 的全部路由(含分组展平后的规则),
331/// 自动为每个端点生成 operation(summary、tag、path 参数、默认响应)。
332///
333/// ## 示例
334///
335/// ```ignore
336/// use sz_rust_router_facade::openapi::{OpenApiBuilder, spec_from_route_config};
337/// use sz_rust_router_facade::routing::load_routes_from_json_str;
338///
339/// let cfg = load_routes_from_json_str(r#"{"routes": [...]}"#).unwrap();
340/// let spec = OpenApiBuilder::new("SZ-Rust API", "1.0.0")
341///     .description("自动扫描路由生成的文档")
342///     .bearer_auth("BearerAuth");
343/// let json = spec_from_route_config(spec, &cfg);
344/// ```
345pub fn spec_from_route_config(
346    builder: OpenApiBuilder,
347    config: &crate::routing::RouteConfig,
348) -> String {
349    let mut spec = builder.build();
350    let routes = config.flatten();
351    routes_to_spec(&routes, &mut spec);
352    serde_json::to_string_pretty(&spec).unwrap_or_else(|_| "{}".to_string())
353}
354
355/// 操作构建器 — 描述单个 API 端点的元数据
356pub struct OperationBuilder {
357    summary: Option<String>,
358    description: Option<String>,
359    tags: Vec<String>,
360    parameters: Vec<Value>,
361    responses: Map<String, Value>,
362    deprecated: bool,
363}
364
365impl OperationBuilder {
366    /// 创建新的操作构建器
367    pub fn new() -> Self {
368        Self {
369            summary: None,
370            description: None,
371            tags: Vec::new(),
372            parameters: Vec::new(),
373            responses: Map::new(),
374            deprecated: false,
375        }
376    }
377
378    /// 设置摘要
379    pub fn summary(&mut self, summary: &str) -> &mut Self {
380        self.summary = Some(summary.to_string());
381        self
382    }
383
384    /// 设置详细描述
385    pub fn description(&mut self, desc: &str) -> &mut Self {
386        self.description = Some(desc.to_string());
387        self
388    }
389
390    /// 添加标签(用于分组)
391    pub fn tag(&mut self, tag: &str) -> &mut Self {
392        self.tags.push(tag.to_string());
393        self
394    }
395
396    /// 添加参数
397    ///
398    /// # 参数
399    ///
400    /// - `name`:参数名
401    /// - `location`:参数位置(`path` / `query` / `header` / `cookie`)
402    /// - `desc`:参数描述
403    /// - `required`:是否必填
404    /// - `schema_type`:数据类型(`string` / `integer` / `number` / `boolean` / `array`)
405    pub fn parameter(
406        &mut self,
407        name: &str,
408        location: &str,
409        desc: &str,
410        required: bool,
411        schema_type: &str,
412    ) -> &mut Self {
413        self.parameters.push(json!({
414            "name": name,
415            "in": location,
416            "description": desc,
417            "required": required,
418            "schema": {
419                "type": schema_type
420            }
421        }));
422        self
423    }
424
425    /// 添加响应
426    ///
427    /// # 参数
428    ///
429    /// - `status`:HTTP 状态码(如 `200`、`404`)
430    /// - `desc`:响应描述
431    /// - `content_type`:内容类型(如 `"application/json"`)
432    pub fn response(&mut self, status: u16, desc: &str, content_type: &str) -> &mut Self {
433        self.responses.insert(
434            status.to_string(),
435            json!({
436                "description": desc,
437                "content": {
438                    content_type: {
439                        "schema": {
440                            "type": "object"
441                        }
442                    }
443                }
444            }),
445        );
446        self
447    }
448
449    /// 添加响应(带 schema 引用)
450    ///
451    /// # 参数
452    ///
453    /// - `status`:HTTP 状态码
454    /// - `desc`:响应描述
455    /// - `content_type`:内容类型
456    /// - `schema_ref`:schema 引用名(如 `"#/components/schemas/User"`)
457    pub fn response_with_schema(
458        &mut self,
459        status: u16,
460        desc: &str,
461        content_type: &str,
462        schema_ref: &str,
463    ) -> &mut Self {
464        self.responses.insert(
465            status.to_string(),
466            json!({
467                "description": desc,
468                "content": {
469                    content_type: {
470                        "schema": {
471                            "$ref": schema_ref
472                        }
473                    }
474                }
475            }),
476        );
477        self
478    }
479
480    /// 标记为已弃用
481    pub fn deprecated(&mut self) -> &mut Self {
482        self.deprecated = true;
483        self
484    }
485
486    /// 构建操作 JSON
487    fn build(self) -> Value {
488        let mut op = Map::new();
489        if let Some(s) = self.summary {
490            op.insert("summary".to_string(), json!(s));
491        }
492        if let Some(d) = self.description {
493            op.insert("description".to_string(), json!(d));
494        }
495        if !self.tags.is_empty() {
496            op.insert("tags".to_string(), json!(self.tags));
497        }
498        if !self.parameters.is_empty() {
499            op.insert("parameters".to_string(), json!(self.parameters));
500        }
501        if !self.responses.is_empty() {
502            op.insert("responses".to_string(), Value::Object(self.responses));
503        } else {
504            // 默认响应
505            op.insert(
506                "responses".to_string(),
507                json!({
508                    "200": {
509                        "description": "成功"
510                    }
511                }),
512            );
513        }
514        if self.deprecated {
515            op.insert("deprecated".to_string(), json!(true));
516        }
517        Value::Object(op)
518    }
519}
520
521impl Default for OperationBuilder {
522    fn default() -> Self {
523        Self::new()
524    }
525}
526
527/// 生成 Swagger UI HTML 页面
528///
529/// 通过 CDN 加载 Swagger UI,将 OpenAPI JSON 内嵌到页面中。
530///
531/// # 参数
532///
533/// - `spec_json`:OpenAPI 规范 JSON 字符串
534pub fn swagger_ui_html(spec_json: &str) -> String {
535    format!(
536        r#"<!DOCTYPE html>
537<html lang="zh-CN">
538<head>
539    <meta charset="UTF-8">
540    <meta name="viewport" content="width=device-width, initial-scale=1.0">
541    <title>API 文档 - Swagger UI</title>
542    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
543    <style>
544        body {{ margin: 0; }}
545    </style>
546</head>
547<body>
548    <div id="swagger-ui"></div>
549    <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
550    <script>
551        window.onload = function() {{
552            const spec = {spec_json};
553            SwaggerUIBundle({{
554                spec: spec,
555                dom_id: '#swagger-ui',
556                presets: [SwaggerUIBundle.presets.apis],
557                layout: 'BaseLayout',
558            }});
559        }};
560    </script>
561</body>
562</html>"#
563    )
564}
565
566/// 生成 Redoc HTML 页面(替代 Swagger UI 的轻量文档查看器)
567///
568/// # 参数
569///
570/// - `spec_json`:OpenAPI 规范 JSON 字符串
571pub fn redoc_html(spec_json: &str) -> String {
572    format!(
573        r#"<!DOCTYPE html>
574<html lang="zh-CN">
575<head>
576    <meta charset="UTF-8">
577    <meta name="viewport" content="width=device-width, initial-scale=1.0">
578    <title>API 文档 - Redoc</title>
579    <style>
580        body {{ margin: 0; padding: 0; }}
581    </style>
582</head>
583<body>
584    <redoc spec-json='{spec_json}'></redoc>
585    <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
586</body>
587</html>"#
588    )
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594
595    #[test]
596    fn test_http_method_as_str() {
597        assert_eq!(HttpMethod::Get.as_str(), "get");
598        assert_eq!(HttpMethod::Post.as_str(), "post");
599        assert_eq!(HttpMethod::Put.as_str(), "put");
600        assert_eq!(HttpMethod::Delete.as_str(), "delete");
601        assert_eq!(HttpMethod::Patch.as_str(), "patch");
602        assert_eq!(HttpMethod::Options.as_str(), "options");
603        assert_eq!(HttpMethod::Head.as_str(), "head");
604    }
605
606    #[test]
607    fn test_openapi_builder_basic() {
608        let spec = OpenApiBuilder::new("Test API", "1.0.0")
609            .description("A test API")
610            .build();
611
612        assert_eq!(spec["openapi"], "3.0.3");
613        assert_eq!(spec["info"]["title"], "Test API");
614        assert_eq!(spec["info"]["version"], "1.0.0");
615        assert_eq!(spec["info"]["description"], "A test API");
616        assert!(spec["paths"].is_object());
617    }
618
619    #[test]
620    fn test_openapi_builder_with_path() {
621        let spec = OpenApiBuilder::new("Test API", "1.0.0")
622            .path("/api/v1/users", HttpMethod::Get, |op| {
623                op.summary("获取用户列表")
624                    .tag("用户")
625                    .response(200, "成功", "application/json");
626            })
627            .build();
628
629        let path = &spec["paths"]["/api/v1/users"]["get"];
630        assert_eq!(path["summary"], "获取用户列表");
631        assert_eq!(path["tags"][0], "用户");
632        assert_eq!(path["responses"]["200"]["description"], "成功");
633    }
634
635    #[test]
636    fn test_openapi_builder_with_parameter() {
637        let spec = OpenApiBuilder::new("Test API", "1.0.0")
638            .path("/api/v1/users/{id}", HttpMethod::Get, |op| {
639                op.summary("获取用户详情")
640                    .parameter("id", "path", "用户 ID", true, "integer")
641                    .response(200, "成功", "application/json")
642                    .response(404, "用户不存在", "application/json");
643            })
644            .build();
645
646        let path = &spec["paths"]["/api/v1/users/{id}"]["get"];
647        assert_eq!(path["parameters"][0]["name"], "id");
648        assert_eq!(path["parameters"][0]["in"], "path");
649        assert_eq!(path["parameters"][0]["required"], true);
650        assert_eq!(path["parameters"][0]["schema"]["type"], "integer");
651        assert_eq!(path["responses"]["404"]["description"], "用户不存在");
652    }
653
654    #[test]
655    fn test_openapi_builder_with_tags() {
656        let spec = OpenApiBuilder::new("Test API", "1.0.0")
657            .tag("用户", "用户管理接口")
658            .tag("订单", "订单管理接口")
659            .build();
660
661        assert_eq!(spec["tags"][0]["name"], "用户");
662        assert_eq!(spec["tags"][0]["description"], "用户管理接口");
663        assert_eq!(spec["tags"][1]["name"], "订单");
664    }
665
666    #[test]
667    fn test_openapi_builder_bearer_auth() {
668        let spec = OpenApiBuilder::new("Test API", "1.0.0")
669            .bearer_auth("BearerAuth")
670            .build();
671
672        let scheme = &spec["components"]["securitySchemes"]["BearerAuth"];
673        assert_eq!(scheme["type"], "http");
674        assert_eq!(scheme["scheme"], "bearer");
675        assert_eq!(scheme["bearerFormat"], "JWT");
676    }
677
678    #[test]
679    fn test_openapi_builder_api_key_auth() {
680        let spec = OpenApiBuilder::new("Test API", "1.0.0")
681            .api_key_auth("ApiKeyAuth", "X-API-Key")
682            .build();
683
684        let scheme = &spec["components"]["securitySchemes"]["ApiKeyAuth"];
685        assert_eq!(scheme["type"], "apiKey");
686        assert_eq!(scheme["in"], "header");
687        assert_eq!(scheme["name"], "X-API-Key");
688    }
689
690    #[test]
691    fn test_openapi_builder_default_response() {
692        let spec = OpenApiBuilder::new("Test API", "1.0.0")
693            .path("/api/v1/health", HttpMethod::Get, |op| {
694                op.summary("健康检查");
695            })
696            .build();
697
698        let path = &spec["paths"]["/api/v1/health"]["get"];
699        // 未指定响应时应有默认 200 响应
700        assert_eq!(path["responses"]["200"]["description"], "成功");
701    }
702
703    #[test]
704    fn test_openapi_builder_deprecated() {
705        let spec = OpenApiBuilder::new("Test API", "1.0.0")
706            .path("/api/v1/old", HttpMethod::Get, |op| {
707                op.summary("旧接口").deprecated();
708            })
709            .build();
710
711        let path = &spec["paths"]["/api/v1/old"]["get"];
712        assert_eq!(path["deprecated"], true);
713    }
714
715    #[test]
716    fn test_openapi_builder_multiple_methods_same_path() {
717        let spec = OpenApiBuilder::new("Test API", "1.0.0")
718            .path("/api/v1/users", HttpMethod::Get, |op| {
719                op.summary("获取列表");
720            })
721            .path("/api/v1/users", HttpMethod::Post, |op| {
722                op.summary("创建用户");
723            })
724            .build();
725
726        let path = &spec["paths"]["/api/v1/users"];
727        assert!(path["get"].is_object());
728        assert!(path["post"].is_object());
729        assert_eq!(path["get"]["summary"], "获取列表");
730        assert_eq!(path["post"]["summary"], "创建用户");
731    }
732
733    #[test]
734    fn test_openapi_builder_to_json_string() {
735        let builder = OpenApiBuilder::new("Test API", "1.0.0");
736        let json = builder.to_json_string();
737        assert!(json.contains("\"openapi\": \"3.0.3\""));
738        assert!(json.contains("\"title\": \"Test API\""));
739        assert!(json.contains("\"version\": \"1.0.0\""));
740    }
741
742    #[test]
743    fn test_openapi_builder_response_with_schema() {
744        let spec = OpenApiBuilder::new("Test API", "1.0.0")
745            .path("/api/v1/users/{id}", HttpMethod::Get, |op| {
746                op.response_with_schema(
747                    200,
748                    "成功",
749                    "application/json",
750                    "#/components/schemas/User",
751                );
752            })
753            .build();
754
755        let schema_ref = &spec["paths"]["/api/v1/users/{id}"]["get"]["responses"]["200"]["content"]
756            ["application/json"]["schema"]["$ref"];
757        assert_eq!(schema_ref, "#/components/schemas/User");
758    }
759
760    #[test]
761    fn test_swagger_ui_html_contains_spec() {
762        let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
763        let html = swagger_ui_html(spec_json);
764        assert!(html.contains("<!DOCTYPE html>"));
765        assert!(html.contains("swagger-ui"));
766        assert!(html.contains(spec_json));
767    }
768
769    #[test]
770    fn test_redoc_html_contains_spec() {
771        let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
772        let html = redoc_html(spec_json);
773        assert!(html.contains("<!DOCTYPE html>"));
774        assert!(html.contains("redoc"));
775        assert!(html.contains(spec_json));
776    }
777
778    #[test]
779    fn test_operation_builder_default() {
780        let op = OperationBuilder::default();
781        assert!(op.summary.is_none());
782        assert!(op.description.is_none());
783        assert!(op.tags.is_empty());
784        assert!(op.parameters.is_empty());
785        assert!(op.responses.is_empty());
786        assert!(!op.deprecated);
787    }
788
789    #[test]
790    fn test_openapi_builder_no_description() {
791        let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
792        assert!(spec["info"]["description"].is_null());
793    }
794
795    #[test]
796    fn test_openapi_builder_no_security_schemes() {
797        let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
798        // 无安全方案时不应有 components
799        assert!(spec.get("components").is_none() || spec["components"].is_null());
800    }
801
802    #[test]
803    fn test_openapi_builder_full_spec() {
804        let spec = OpenApiBuilder::new("SZ-Rust API", "1.0.0")
805            .description("全栈 API 文档")
806            .tag("用户", "用户管理")
807            .tag("认证", "认证授权")
808            .bearer_auth("BearerAuth")
809            .path("/api/v1/auth/login", HttpMethod::Post, |op| {
810                op.summary("用户登录")
811                    .description("通过用户名密码获取 JWT Token")
812                    .tag("认证")
813                    .parameter("username", "query", "用户名", true, "string")
814                    .parameter("password", "query", "密码", true, "string")
815                    .response(200, "登录成功", "application/json")
816                    .response(401, "认证失败", "application/json");
817            })
818            .path("/api/v1/users", HttpMethod::Get, |op| {
819                op.summary("获取用户列表").tag("用户").response_with_schema(
820                    200,
821                    "成功",
822                    "application/json",
823                    "#/components/schemas/UserList",
824                );
825            })
826            .path("/api/v1/users/{id}", HttpMethod::Delete, |op| {
827                op.summary("删除用户")
828                    .tag("用户")
829                    .parameter("id", "path", "用户 ID", true, "integer")
830                    .response(204, "删除成功", "application/json")
831                    .response(404, "用户不存在", "application/json");
832            })
833            .build();
834
835        // 验证基本结构
836        assert_eq!(spec["openapi"], "3.0.3");
837        assert_eq!(spec["info"]["title"], "SZ-Rust API");
838        assert_eq!(spec["info"]["description"], "全栈 API 文档");
839
840        // 验证路径
841        assert_eq!(spec["paths"].as_object().unwrap().len(), 3);
842
843        // 验证标签
844        assert_eq!(spec["tags"].as_array().unwrap().len(), 2);
845
846        // 验证安全方案
847        assert_eq!(
848            spec["components"]["securitySchemes"]["BearerAuth"]["scheme"],
849            "bearer"
850        );
851
852        // 验证 login 端点
853        let login = &spec["paths"]["/api/v1/auth/login"]["post"];
854        assert_eq!(login["summary"], "用户登录");
855        assert_eq!(login["description"], "通过用户名密码获取 JWT Token");
856        assert_eq!(login["tags"][0], "认证");
857        assert_eq!(login["parameters"].as_array().unwrap().len(), 2);
858        assert_eq!(login["responses"]["401"]["description"], "认证失败");
859
860        // 验证 delete 端点
861        let delete = &spec["paths"]["/api/v1/users/{id}"]["delete"];
862        assert_eq!(delete["summary"], "删除用户");
863        assert_eq!(delete["responses"]["204"]["description"], "删除成功");
864    }
865
866    // ---- 路由自动扫描 ----
867
868    fn sample_route_config() -> crate::routing::RouteConfig {
869        use crate::routing::{HttpMethod, RouteConfig, RouteRule};
870        let mut cfg = RouteConfig::new();
871        cfg.add_route(RouteRule::new(
872            HttpMethod::GET,
873            "/api/v1/users",
874            "User@list",
875        ));
876        cfg.add_route(RouteRule::new(
877            HttpMethod::POST,
878            "/api/v1/users",
879            "User@create",
880        ));
881        cfg.add_route(RouteRule::new(
882            HttpMethod::GET,
883            "/api/v1/users/{id}",
884            "User@detail",
885        ));
886        cfg.add_route(RouteRule::new(
887            HttpMethod::DELETE,
888            "/api/v1/users/{id}",
889            "User@delete",
890        ));
891        cfg
892    }
893
894    #[test]
895    fn test_routes_to_spec_generates_paths() {
896        let cfg = sample_route_config();
897        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
898        routes_to_spec(&cfg.flatten(), &mut spec);
899
900        let paths = spec["paths"].as_object().unwrap();
901        assert_eq!(paths.len(), 2, "应有 2 个路径(/users 与 /users/{{id}})");
902        assert!(paths.contains_key("/api/v1/users"));
903        assert!(paths.contains_key("/api/v1/users/{id}"));
904    }
905
906    #[test]
907    fn test_routes_to_spec_method_mapping() {
908        let cfg = sample_route_config();
909        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
910        routes_to_spec(&cfg.flatten(), &mut spec);
911
912        let users = &spec["paths"]["/api/v1/users"];
913        assert!(users["get"].is_object(), "GET 应存在");
914        assert!(users["post"].is_object(), "POST 应存在");
915        assert_eq!(users["get"]["summary"], "User@list");
916        assert_eq!(users["post"]["summary"], "User@create");
917    }
918
919    #[test]
920    fn test_routes_to_spec_auto_detect_path_params() {
921        let cfg = sample_route_config();
922        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
923        routes_to_spec(&cfg.flatten(), &mut spec);
924
925        let detail = &spec["paths"]["/api/v1/users/{id}"]["get"];
926        let params = detail["parameters"].as_array().unwrap();
927        assert_eq!(params.len(), 1);
928        assert_eq!(params[0]["name"], "id");
929        assert_eq!(params[0]["in"], "path");
930        assert_eq!(params[0]["required"], true);
931    }
932
933    #[test]
934    fn test_routes_to_spec_tag_from_path() {
935        let cfg = sample_route_config();
936        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
937        routes_to_spec(&cfg.flatten(), &mut spec);
938
939        let users = &spec["paths"]["/api/v1/users"]["get"];
940        assert_eq!(
941            users["tags"][0], "api",
942            "tag 应取路径首段(/api/v1/users → api)"
943        );
944    }
945
946    #[test]
947    fn test_routes_to_spec_tags_collected_and_deduped() {
948        let cfg = sample_route_config();
949        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
950        routes_to_spec(&cfg.flatten(), &mut spec);
951
952        let tags = spec["tags"].as_array().unwrap();
953        let names: Vec<&str> = tags.iter().filter_map(|t| t["name"].as_str()).collect();
954        assert_eq!(names, vec!["api"], "同一 tag 不应重复收集");
955    }
956
957    #[test]
958    fn test_routes_to_spec_middleware_in_description() {
959        use crate::routing::{HttpMethod, RouteRule};
960        let mut cfg = crate::routing::RouteConfig::new();
961        cfg.add_route(
962            RouteRule::new(HttpMethod::GET, "/api/v1/admin", "Admin@index").with_middleware("auth"),
963        );
964        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
965        routes_to_spec(&cfg.flatten(), &mut spec);
966
967        let op = &spec["paths"]["/api/v1/admin"]["get"];
968        assert_eq!(op["description"], "中间件: auth");
969    }
970
971    #[test]
972    fn test_routes_to_spec_default_responses() {
973        let cfg = sample_route_config();
974        let mut spec = OpenApiBuilder::new("Scan Test", "1.0.0").build();
975        routes_to_spec(&cfg.flatten(), &mut spec);
976
977        let detail = &spec["paths"]["/api/v1/users/{id}"]["get"];
978        assert_eq!(detail["responses"]["200"]["description"], "成功");
979        assert_eq!(detail["responses"]["404"]["description"], "资源不存在");
980    }
981
982    #[test]
983    fn test_spec_from_route_config_full_flow() {
984        let cfg = sample_route_config();
985        let builder = OpenApiBuilder::new("Scan Full", "1.0.0")
986            .description("自动扫描")
987            .bearer_auth("BearerAuth");
988        let json = spec_from_route_config(builder, &cfg);
989
990        let spec: Value = serde_json::from_str(&json).unwrap();
991        assert_eq!(spec["openapi"], "3.0.3");
992        assert_eq!(spec["info"]["title"], "Scan Full");
993        assert_eq!(spec["paths"].as_object().unwrap().len(), 2);
994        assert_eq!(
995            spec["components"]["securitySchemes"]["BearerAuth"]["scheme"],
996            "bearer"
997        );
998    }
999
1000    #[test]
1001    fn test_routes_to_spec_merges_with_existing_paths() {
1002        // 手工注册的路径与扫描生成的路径应共存,不互相覆盖
1003        let mut spec = OpenApiBuilder::new("Merge Test", "1.0.0")
1004            .path("/api/v1/health", HttpMethod::Get, |op| {
1005                op.summary("健康检查");
1006            })
1007            .build();
1008
1009        let cfg = sample_route_config();
1010        routes_to_spec(&cfg.flatten(), &mut spec);
1011
1012        let paths = spec["paths"].as_object().unwrap();
1013        assert_eq!(paths.len(), 3);
1014        assert!(paths.contains_key("/api/v1/health"));
1015        assert_eq!(paths["/api/v1/health"]["get"]["summary"], "健康检查");
1016    }
1017}