silent_openapi/
schema.rs

1//! OpenAPI 文档模式和工具
2//!
3//! 提供创建和管理OpenAPI文档的工具函数和类型定义。
4
5use crate::{OpenApiError, Result};
6use serde_json::Value;
7use utoipa::openapi::{InfoBuilder, OpenApi, OpenApiBuilder, PathItem, PathsBuilder};
8
9/// OpenAPI文档构建器
10///
11/// 提供便捷的方法来构建和管理OpenAPI文档。
12#[derive(Clone)]
13pub struct OpenApiDoc {
14    /// 内部的OpenAPI对象
15    openapi: OpenApi,
16}
17
18impl OpenApiDoc {
19    /// 创建一个新的OpenAPI文档
20    ///
21    /// # 参数
22    ///
23    /// - `title`: API标题
24    /// - `version`: API版本
25    ///
26    /// # 示例
27    ///
28    /// ```rust
29    /// use silent_openapi::OpenApiDoc;
30    ///
31    /// let doc = OpenApiDoc::new("用户API", "1.0.0");
32    /// ```
33    pub fn new(title: &str, version: &str) -> Self {
34        let openapi = OpenApiBuilder::new()
35            .info(InfoBuilder::new().title(title).version(version).build())
36            .build();
37
38        Self { openapi }
39    }
40
41    /// 由现有的 OpenApi 对象创建文档包装
42    pub fn from_openapi(openapi: OpenApi) -> Self {
43        Self { openapi }
44    }
45
46    /// 设置API描述
47    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
48        if let Some(ref mut info) = self.openapi.info.description {
49            *info = description.into();
50        } else {
51            self.openapi.info.description = Some(description.into());
52        }
53        self
54    }
55
56    /// 添加服务器信息
57    ///
58    /// # 参数
59    ///
60    /// - `url`: 服务器URL
61    /// - `description`: 服务器描述
62    pub fn add_server(mut self, url: &str, description: Option<&str>) -> Self {
63        use utoipa::openapi::ServerBuilder;
64
65        let mut server_builder = ServerBuilder::new().url(url);
66        if let Some(desc) = description {
67            server_builder = server_builder.description(Some(desc));
68        }
69
70        let server = server_builder.build();
71
72        if self.openapi.servers.is_none() {
73            self.openapi.servers = Some(Vec::new());
74        }
75
76        if let Some(ref mut servers) = self.openapi.servers {
77            servers.push(server);
78        }
79
80        self
81    }
82
83    /// 添加路径项
84    ///
85    /// # 参数
86    ///
87    /// - `path`: API路径
88    /// - `path_item`: 路径项定义
89    pub fn add_path(mut self, path: &str, path_item: PathItem) -> Self {
90        // 简化实现:创建一个新的paths对象
91        let mut paths_builder = PathsBuilder::new();
92        paths_builder = paths_builder.path(path, path_item);
93        self.openapi.paths = paths_builder.build();
94        self
95    }
96
97    /// 批量添加路径
98    ///
99    /// # 参数
100    ///
101    /// - `paths`: 路径映射表
102    pub fn add_paths(mut self, paths: Vec<(String, PathItem)>) -> Self {
103        let mut paths_builder = PathsBuilder::new();
104        for (path, path_item) in paths {
105            paths_builder = paths_builder.path(&path, path_item);
106        }
107        self.openapi.paths = paths_builder.build();
108        self
109    }
110
111    /// 为给定类型名追加占位 schema(占位 Object,用于引用解析)
112    pub fn add_placeholder_schemas(mut self, type_names: &[&str]) -> Self {
113        use utoipa::openapi::ComponentsBuilder;
114        use utoipa::openapi::schema::{ObjectBuilder, Schema};
115        let mut components = self
116            .openapi
117            .components
118            .unwrap_or_else(|| ComponentsBuilder::new().build());
119        for name in type_names {
120            components
121                .schemas
122                .entry((*name).to_string())
123                .or_insert_with(|| {
124                    utoipa::openapi::RefOr::T(Schema::Object(ObjectBuilder::new().build()))
125                });
126        }
127        self.openapi.components = Some(components);
128        self
129    }
130
131    /// 应用由 endpoint 宏注册的 ToSchema 完整 schema
132    pub fn apply_registered_schemas(mut self) -> Self {
133        crate::doc::apply_registered_schemas(&mut self.openapi);
134        self
135    }
136
137    /// 添加 Bearer/JWT 安全定义
138    pub fn add_bearer_auth(mut self, scheme_name: &str, description: Option<&str>) -> Self {
139        use utoipa::openapi::ComponentsBuilder;
140        use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
141
142        let http = HttpBuilder::new()
143            .scheme(HttpAuthScheme::Bearer)
144            .bearer_format("JWT");
145        if let Some(_desc) = description {
146            // 某些版本的 utoipa 暂不支持在 HttpBuilder 直接设置 description,这里跳过
147        }
148        let scheme = SecurityScheme::Http(http.build());
149
150        let mut components = self
151            .openapi
152            .components
153            .unwrap_or_else(|| ComponentsBuilder::new().build());
154        components
155            .security_schemes
156            .insert(scheme_name.to_string(), scheme);
157        self.openapi.components = Some(components);
158        self
159    }
160
161    /// 设置全局 security 要求
162    pub fn set_global_security(mut self, scheme_name: &str, scopes: &[&str]) -> Self {
163        use utoipa::openapi::security::SecurityRequirement;
164
165        let scopes_vec: Vec<String> = scopes.iter().map(|s| s.to_string()).collect();
166        let requirement = SecurityRequirement::new(scheme_name.to_string(), scopes_vec);
167
168        match self.openapi.security {
169            Some(ref mut list) => list.push(requirement),
170            None => self.openapi.security = Some(vec![requirement]),
171        }
172        self
173    }
174
175    /// 获取内部的OpenAPI对象
176    pub fn openapi(&self) -> &OpenApi {
177        &self.openapi
178    }
179
180    /// 转换为OpenAPI对象
181    pub fn into_openapi(self) -> OpenApi {
182        self.openapi
183    }
184
185    /// 序列化为JSON字符串
186    pub fn to_json(&self) -> Result<String> {
187        serde_json::to_string(&self.openapi).map_err(OpenApiError::Json)
188    }
189
190    /// 序列化为格式化的JSON字符串
191    pub fn to_pretty_json(&self) -> Result<String> {
192        serde_json::to_string_pretty(&self.openapi).map_err(OpenApiError::Json)
193    }
194
195    /// 序列化为JSON Value
196    pub fn to_json_value(&self) -> Result<Value> {
197        serde_json::to_value(&self.openapi).map_err(OpenApiError::Json)
198    }
199}
200
201/// 路径信息
202///
203/// 用于描述API路径的基本信息。
204#[derive(Debug, Clone)]
205pub struct PathInfo {
206    /// HTTP方法
207    pub method: http::Method,
208    /// 路径模式
209    pub path: String,
210    /// 操作ID
211    pub operation_id: Option<String>,
212    /// 摘要
213    pub summary: Option<String>,
214    /// 描述
215    pub description: Option<String>,
216    /// 标签
217    pub tags: Vec<String>,
218}
219
220impl PathInfo {
221    /// 创建新的路径信息
222    pub fn new(method: http::Method, path: &str) -> Self {
223        Self {
224            method,
225            path: path.to_string(),
226            operation_id: None,
227            summary: None,
228            description: None,
229            tags: Vec::new(),
230        }
231    }
232
233    /// 设置操作ID
234    pub fn operation_id<S: Into<String>>(mut self, id: S) -> Self {
235        self.operation_id = Some(id.into());
236        self
237    }
238
239    /// 设置摘要
240    pub fn summary<S: Into<String>>(mut self, summary: S) -> Self {
241        self.summary = Some(summary.into());
242        self
243    }
244
245    /// 设置描述
246    pub fn description<S: Into<String>>(mut self, description: S) -> Self {
247        self.description = Some(description.into());
248        self
249    }
250
251    /// 添加标签
252    pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
253        self.tags.push(tag.into());
254        self
255    }
256
257    /// 设置多个标签
258    pub fn tags<I, S>(mut self, tags: I) -> Self
259    where
260        I: IntoIterator<Item = S>,
261        S: Into<String>,
262    {
263        self.tags = tags.into_iter().map(|s| s.into()).collect();
264        self
265    }
266}
267
268/// 创建基础的成功响应
269pub fn create_success_response(description: &str) -> utoipa::openapi::Response {
270    use utoipa::openapi::ResponseBuilder;
271
272    ResponseBuilder::new().description(description).build()
273}
274
275/// 创建JSON响应
276pub fn create_json_response(
277    description: &str,
278    _schema_ref: Option<&str>,
279) -> utoipa::openapi::Response {
280    use utoipa::openapi::ResponseBuilder;
281
282    // 简化实现,暂时不处理schema引用
283    ResponseBuilder::new().description(description).build()
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_openapi_doc_creation() {
292        let doc = OpenApiDoc::new("Test API", "1.0.0")
293            .description("A test API")
294            .add_server("http://localhost:8080", Some("Development server"));
295
296        let openapi = doc.openapi();
297        assert_eq!(openapi.info.title, "Test API");
298        assert_eq!(openapi.info.version, "1.0.0");
299        assert_eq!(openapi.info.description, Some("A test API".to_string()));
300        assert!(openapi.servers.is_some());
301    }
302
303    #[test]
304    fn test_path_info() {
305        let path_info = PathInfo::new(http::Method::GET, "/users/{id}")
306            .operation_id("get_user")
307            .summary("Get user by ID")
308            .description("Retrieve a user by their unique identifier")
309            .tag("users");
310
311        assert_eq!(path_info.method, http::Method::GET);
312        assert_eq!(path_info.path, "/users/{id}");
313        assert_eq!(path_info.operation_id, Some("get_user".to_string()));
314        assert_eq!(path_info.tags, vec!["users"]);
315    }
316
317    #[test]
318    fn test_json_serialization() {
319        let doc = OpenApiDoc::new("Test API", "1.0.0");
320        let json = doc.to_json().unwrap();
321        assert!(json.contains("Test API"));
322        assert!(json.contains("1.0.0"));
323    }
324
325    #[test]
326    fn test_add_server_and_security() {
327        let doc = OpenApiDoc::new("T", "1")
328            .add_server("https://api.example.com", Some("prod"))
329            .add_bearer_auth("bearerAuth", Some("jwt"))
330            .set_global_security("bearerAuth", &[]);
331        let json_value = doc.to_json_value().unwrap();
332        // 服务器与安全定义存在
333        assert!(json_value["servers"].is_array());
334        assert!(json_value["components"]["securitySchemes"]["bearerAuth"].is_object());
335        assert!(json_value["security"].is_array());
336    }
337
338    #[test]
339    fn test_add_paths_multiple_and_pretty_json() {
340        let pi = PathItem::default();
341        let doc = OpenApiDoc::new("T", "1").add_paths(vec![("/ping".into(), pi)]);
342        let pretty = doc.to_pretty_json().unwrap();
343        assert!(pretty.contains("/ping"));
344    }
345}