1use serde_json::{json, Map, Value};
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum HttpMethod {
47 Get,
49 Post,
51 Put,
53 Delete,
55 Patch,
57 Options,
59 Head,
61}
62
63impl HttpMethod {
64 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
78pub 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 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 pub fn description(mut self, desc: &str) -> Self {
110 self.description = Some(desc.to_string());
111 self
112 }
113
114 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 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 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 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 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 pub fn to_json_string(self) -> String {
209 serde_json::to_string_pretty(&self.build()).unwrap_or_else(|_| "{}".to_string())
210 }
211}
212
213pub 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 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 pub fn summary(&mut self, summary: &str) -> &mut Self {
238 self.summary = Some(summary.to_string());
239 self
240 }
241
242 pub fn description(&mut self, desc: &str) -> &mut Self {
244 self.description = Some(desc.to_string());
245 self
246 }
247
248 pub fn tag(&mut self, tag: &str) -> &mut Self {
250 self.tags.push(tag.to_string());
251 self
252 }
253
254 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 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 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 pub fn deprecated(&mut self) -> &mut Self {
340 self.deprecated = true;
341 self
342 }
343
344 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 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
385pub 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
424pub 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 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 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 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 assert_eq!(spec["paths"].as_object().unwrap().len(), 3);
700
701 assert_eq!(spec["tags"].as_array().unwrap().len(), 2);
703
704 assert_eq!(
706 spec["components"]["securitySchemes"]["BearerAuth"]["scheme"],
707 "bearer"
708 );
709
710 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 let delete = &spec["paths"]["/api/v1/users/{id}"]["delete"];
720 assert_eq!(delete["summary"], "删除用户");
721 assert_eq!(delete["responses"]["204"]["description"], "删除成功");
722 }
723}