use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Options,
Head,
}
impl HttpMethod {
pub fn as_str(&self) -> &'static str {
match self {
HttpMethod::Get => "get",
HttpMethod::Post => "post",
HttpMethod::Put => "put",
HttpMethod::Delete => "delete",
HttpMethod::Patch => "patch",
HttpMethod::Options => "options",
HttpMethod::Head => "head",
}
}
}
pub struct OpenApiBuilder {
title: String,
version: String,
description: Option<String>,
paths: Map<String, Value>,
tags: Vec<Value>,
security_schemes: Map<String, Value>,
}
impl OpenApiBuilder {
pub fn new(title: &str, version: &str) -> Self {
Self {
title: title.to_string(),
version: version.to_string(),
description: None,
paths: Map::new(),
tags: Vec::new(),
security_schemes: Map::new(),
}
}
pub fn description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
pub fn tag(mut self, name: &str, description: &str) -> Self {
self.tags.push(json!({
"name": name,
"description": description,
}));
self
}
pub fn path<F>(mut self, path: &str, method: HttpMethod, config: F) -> Self
where
F: FnOnce(&mut OperationBuilder),
{
let mut op = OperationBuilder::new();
config(&mut op);
let path_entry = self
.paths
.entry(path.to_string())
.or_insert_with(|| json!({}));
if let Value::Object(ref mut obj) = path_entry {
obj.insert(method.as_str().to_string(), op.build());
}
self
}
pub fn bearer_auth(mut self, scheme_name: &str) -> Self {
self.security_schemes.insert(
scheme_name.to_string(),
json!({
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
}),
);
self
}
pub fn api_key_auth(mut self, scheme_name: &str, header_name: &str) -> Self {
self.security_schemes.insert(
scheme_name.to_string(),
json!({
"type": "apiKey",
"in": "header",
"name": header_name,
}),
);
self
}
pub fn build(self) -> Value {
let mut info = json!({
"title": self.title,
"version": self.version,
});
if let Some(desc) = self.description {
info["description"] = json!(desc);
}
let mut spec = json!({
"openapi": "3.0.3",
"info": info,
"paths": self.paths,
});
let mut components = Map::new();
if !self.security_schemes.is_empty() {
components.insert(
"securitySchemes".to_string(),
Value::Object(self.security_schemes),
);
}
if !components.is_empty() {
spec["components"] = Value::Object(components);
}
if !self.tags.is_empty() {
spec["tags"] = Value::Array(self.tags);
}
spec
}
pub fn to_json_string(self) -> String {
serde_json::to_string_pretty(&self.build()).unwrap_or_else(|_| "{}".to_string())
}
}
pub struct OperationBuilder {
summary: Option<String>,
description: Option<String>,
tags: Vec<String>,
parameters: Vec<Value>,
responses: Map<String, Value>,
deprecated: bool,
}
impl OperationBuilder {
pub fn new() -> Self {
Self {
summary: None,
description: None,
tags: Vec::new(),
parameters: Vec::new(),
responses: Map::new(),
deprecated: false,
}
}
pub fn summary(&mut self, summary: &str) -> &mut Self {
self.summary = Some(summary.to_string());
self
}
pub fn description(&mut self, desc: &str) -> &mut Self {
self.description = Some(desc.to_string());
self
}
pub fn tag(&mut self, tag: &str) -> &mut Self {
self.tags.push(tag.to_string());
self
}
pub fn parameter(
&mut self,
name: &str,
location: &str,
desc: &str,
required: bool,
schema_type: &str,
) -> &mut Self {
self.parameters.push(json!({
"name": name,
"in": location,
"description": desc,
"required": required,
"schema": {
"type": schema_type
}
}));
self
}
pub fn response(&mut self, status: u16, desc: &str, content_type: &str) -> &mut Self {
self.responses.insert(
status.to_string(),
json!({
"description": desc,
"content": {
content_type: {
"schema": {
"type": "object"
}
}
}
}),
);
self
}
pub fn response_with_schema(
&mut self,
status: u16,
desc: &str,
content_type: &str,
schema_ref: &str,
) -> &mut Self {
self.responses.insert(
status.to_string(),
json!({
"description": desc,
"content": {
content_type: {
"schema": {
"$ref": schema_ref
}
}
}
}),
);
self
}
pub fn deprecated(&mut self) -> &mut Self {
self.deprecated = true;
self
}
fn build(self) -> Value {
let mut op = Map::new();
if let Some(s) = self.summary {
op.insert("summary".to_string(), json!(s));
}
if let Some(d) = self.description {
op.insert("description".to_string(), json!(d));
}
if !self.tags.is_empty() {
op.insert("tags".to_string(), json!(self.tags));
}
if !self.parameters.is_empty() {
op.insert("parameters".to_string(), json!(self.parameters));
}
if !self.responses.is_empty() {
op.insert("responses".to_string(), Value::Object(self.responses));
} else {
op.insert(
"responses".to_string(),
json!({
"200": {
"description": "成功"
}
}),
);
}
if self.deprecated {
op.insert("deprecated".to_string(), json!(true));
}
Value::Object(op)
}
}
impl Default for OperationBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn swagger_ui_html(spec_json: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 - Swagger UI</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
<style>
body {{ margin: 0; }}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
window.onload = function() {{
const spec = {spec_json};
SwaggerUIBundle({{
spec: spec,
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis],
layout: 'BaseLayout',
}});
}};
</script>
</body>
</html>"#
)
}
pub fn redoc_html(spec_json: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 - Redoc</title>
<style>
body {{ margin: 0; padding: 0; }}
</style>
</head>
<body>
<redoc spec-json='{spec_json}'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
</body>
</html>"#
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_method_as_str() {
assert_eq!(HttpMethod::Get.as_str(), "get");
assert_eq!(HttpMethod::Post.as_str(), "post");
assert_eq!(HttpMethod::Put.as_str(), "put");
assert_eq!(HttpMethod::Delete.as_str(), "delete");
assert_eq!(HttpMethod::Patch.as_str(), "patch");
assert_eq!(HttpMethod::Options.as_str(), "options");
assert_eq!(HttpMethod::Head.as_str(), "head");
}
#[test]
fn test_openapi_builder_basic() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.description("A test API")
.build();
assert_eq!(spec["openapi"], "3.0.3");
assert_eq!(spec["info"]["title"], "Test API");
assert_eq!(spec["info"]["version"], "1.0.0");
assert_eq!(spec["info"]["description"], "A test API");
assert!(spec["paths"].is_object());
}
#[test]
fn test_openapi_builder_with_path() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/users", HttpMethod::Get, |op| {
op.summary("获取用户列表")
.tag("用户")
.response(200, "成功", "application/json");
})
.build();
let path = &spec["paths"]["/api/v1/users"]["get"];
assert_eq!(path["summary"], "获取用户列表");
assert_eq!(path["tags"][0], "用户");
assert_eq!(path["responses"]["200"]["description"], "成功");
}
#[test]
fn test_openapi_builder_with_parameter() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/users/{id}", HttpMethod::Get, |op| {
op.summary("获取用户详情")
.parameter("id", "path", "用户 ID", true, "integer")
.response(200, "成功", "application/json")
.response(404, "用户不存在", "application/json");
})
.build();
let path = &spec["paths"]["/api/v1/users/{id}"]["get"];
assert_eq!(path["parameters"][0]["name"], "id");
assert_eq!(path["parameters"][0]["in"], "path");
assert_eq!(path["parameters"][0]["required"], true);
assert_eq!(path["parameters"][0]["schema"]["type"], "integer");
assert_eq!(path["responses"]["404"]["description"], "用户不存在");
}
#[test]
fn test_openapi_builder_with_tags() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.tag("用户", "用户管理接口")
.tag("订单", "订单管理接口")
.build();
assert_eq!(spec["tags"][0]["name"], "用户");
assert_eq!(spec["tags"][0]["description"], "用户管理接口");
assert_eq!(spec["tags"][1]["name"], "订单");
}
#[test]
fn test_openapi_builder_bearer_auth() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.bearer_auth("BearerAuth")
.build();
let scheme = &spec["components"]["securitySchemes"]["BearerAuth"];
assert_eq!(scheme["type"], "http");
assert_eq!(scheme["scheme"], "bearer");
assert_eq!(scheme["bearerFormat"], "JWT");
}
#[test]
fn test_openapi_builder_api_key_auth() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.api_key_auth("ApiKeyAuth", "X-API-Key")
.build();
let scheme = &spec["components"]["securitySchemes"]["ApiKeyAuth"];
assert_eq!(scheme["type"], "apiKey");
assert_eq!(scheme["in"], "header");
assert_eq!(scheme["name"], "X-API-Key");
}
#[test]
fn test_openapi_builder_default_response() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/health", HttpMethod::Get, |op| {
op.summary("健康检查");
})
.build();
let path = &spec["paths"]["/api/v1/health"]["get"];
assert_eq!(path["responses"]["200"]["description"], "成功");
}
#[test]
fn test_openapi_builder_deprecated() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/old", HttpMethod::Get, |op| {
op.summary("旧接口").deprecated();
})
.build();
let path = &spec["paths"]["/api/v1/old"]["get"];
assert_eq!(path["deprecated"], true);
}
#[test]
fn test_openapi_builder_multiple_methods_same_path() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/users", HttpMethod::Get, |op| {
op.summary("获取列表");
})
.path("/api/v1/users", HttpMethod::Post, |op| {
op.summary("创建用户");
})
.build();
let path = &spec["paths"]["/api/v1/users"];
assert!(path["get"].is_object());
assert!(path["post"].is_object());
assert_eq!(path["get"]["summary"], "获取列表");
assert_eq!(path["post"]["summary"], "创建用户");
}
#[test]
fn test_openapi_builder_to_json_string() {
let builder = OpenApiBuilder::new("Test API", "1.0.0");
let json = builder.to_json_string();
assert!(json.contains("\"openapi\": \"3.0.3\""));
assert!(json.contains("\"title\": \"Test API\""));
assert!(json.contains("\"version\": \"1.0.0\""));
}
#[test]
fn test_openapi_builder_response_with_schema() {
let spec = OpenApiBuilder::new("Test API", "1.0.0")
.path("/api/v1/users/{id}", HttpMethod::Get, |op| {
op.response_with_schema(
200,
"成功",
"application/json",
"#/components/schemas/User",
);
})
.build();
let schema_ref = &spec["paths"]["/api/v1/users/{id}"]["get"]["responses"]["200"]["content"]
["application/json"]["schema"]["$ref"];
assert_eq!(schema_ref, "#/components/schemas/User");
}
#[test]
fn test_swagger_ui_html_contains_spec() {
let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
let html = swagger_ui_html(spec_json);
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("swagger-ui"));
assert!(html.contains(spec_json));
}
#[test]
fn test_redoc_html_contains_spec() {
let spec_json = r#"{"openapi":"3.0.3","info":{"title":"Test"}}"#;
let html = redoc_html(spec_json);
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("redoc"));
assert!(html.contains(spec_json));
}
#[test]
fn test_operation_builder_default() {
let op = OperationBuilder::default();
assert!(op.summary.is_none());
assert!(op.description.is_none());
assert!(op.tags.is_empty());
assert!(op.parameters.is_empty());
assert!(op.responses.is_empty());
assert!(!op.deprecated);
}
#[test]
fn test_openapi_builder_no_description() {
let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
assert!(spec["info"]["description"].is_null());
}
#[test]
fn test_openapi_builder_no_security_schemes() {
let spec = OpenApiBuilder::new("Test API", "1.0.0").build();
assert!(spec.get("components").is_none() || spec["components"].is_null());
}
#[test]
fn test_openapi_builder_full_spec() {
let spec = OpenApiBuilder::new("SZ-Rust API", "1.0.0")
.description("全栈 API 文档")
.tag("用户", "用户管理")
.tag("认证", "认证授权")
.bearer_auth("BearerAuth")
.path("/api/v1/auth/login", HttpMethod::Post, |op| {
op.summary("用户登录")
.description("通过用户名密码获取 JWT Token")
.tag("认证")
.parameter("username", "query", "用户名", true, "string")
.parameter("password", "query", "密码", true, "string")
.response(200, "登录成功", "application/json")
.response(401, "认证失败", "application/json");
})
.path("/api/v1/users", HttpMethod::Get, |op| {
op.summary("获取用户列表").tag("用户").response_with_schema(
200,
"成功",
"application/json",
"#/components/schemas/UserList",
);
})
.path("/api/v1/users/{id}", HttpMethod::Delete, |op| {
op.summary("删除用户")
.tag("用户")
.parameter("id", "path", "用户 ID", true, "integer")
.response(204, "删除成功", "application/json")
.response(404, "用户不存在", "application/json");
})
.build();
assert_eq!(spec["openapi"], "3.0.3");
assert_eq!(spec["info"]["title"], "SZ-Rust API");
assert_eq!(spec["info"]["description"], "全栈 API 文档");
assert_eq!(spec["paths"].as_object().unwrap().len(), 3);
assert_eq!(spec["tags"].as_array().unwrap().len(), 2);
assert_eq!(
spec["components"]["securitySchemes"]["BearerAuth"]["scheme"],
"bearer"
);
let login = &spec["paths"]["/api/v1/auth/login"]["post"];
assert_eq!(login["summary"], "用户登录");
assert_eq!(login["description"], "通过用户名密码获取 JWT Token");
assert_eq!(login["tags"][0], "认证");
assert_eq!(login["parameters"].as_array().unwrap().len(), 2);
assert_eq!(login["responses"]["401"]["description"], "认证失败");
let delete = &spec["paths"]["/api/v1/users/{id}"]["delete"];
assert_eq!(delete["summary"], "删除用户");
assert_eq!(delete["responses"]["204"]["description"], "删除成功");
}
}