Skip to main content

silent_openapi/
doc.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use http::Method;
5use once_cell::sync::Lazy;
6
7use silent::prelude::{HandlerGetter, Route};
8use silent::{
9    Handler, HandlerWrapper, Request as SilentRequest, Response as SilentResponse,
10    Result as SilentResult,
11};
12use utoipa::openapi::{Components, ComponentsBuilder, OpenApi};
13
14/// 用于标注接口文档的元信息
15#[derive(Clone, Debug)]
16pub struct DocMeta {
17    pub summary: Option<String>,
18    pub description: Option<String>,
19    pub deprecated: bool,
20    pub tags: Vec<String>,
21}
22
23static DOC_REGISTRY: Lazy<Mutex<HashMap<usize, DocMeta>>> =
24    Lazy::new(|| Mutex::new(HashMap::new()));
25
26pub fn register_doc_by_ptr(ptr: usize, summary: Option<&str>, description: Option<&str>) {
27    let mut map = DOC_REGISTRY.lock().expect("doc registry poisoned");
28    map.insert(
29        ptr,
30        DocMeta {
31            summary: summary.map(|s| s.to_string()),
32            description: description.map(|s| s.to_string()),
33            deprecated: false,
34            tags: Vec::new(),
35        },
36    );
37}
38
39/// 注册文档元信息(带 deprecated 和 tags 支持)
40pub fn register_doc_by_ptr_ext(
41    ptr: usize,
42    summary: Option<&str>,
43    description: Option<&str>,
44    deprecated: bool,
45    tags: &[&str],
46) {
47    let mut map = DOC_REGISTRY.lock().expect("doc registry poisoned");
48    map.insert(
49        ptr,
50        DocMeta {
51            summary: summary.map(|s| s.to_string()),
52            description: description.map(|s| s.to_string()),
53            deprecated,
54            tags: tags.iter().map(|s| s.to_string()).collect(),
55        },
56    );
57}
58
59pub(crate) fn lookup_doc_by_handler_ptr(ptr: usize) -> Option<DocMeta> {
60    DOC_REGISTRY.lock().ok().and_then(|m| m.get(&ptr).cloned())
61}
62
63/// 响应类型元信息
64#[derive(Clone, Debug)]
65pub enum ResponseMeta {
66    TextPlain,
67    Json { type_name: &'static str },
68}
69
70static RESPONSE_REGISTRY: Lazy<Mutex<HashMap<usize, ResponseMeta>>> =
71    Lazy::new(|| Mutex::new(HashMap::new()));
72
73pub fn register_response_by_ptr(ptr: usize, meta: ResponseMeta) {
74    let mut map = RESPONSE_REGISTRY
75        .lock()
76        .expect("response registry poisoned");
77    map.insert(ptr, meta);
78}
79
80pub(crate) fn lookup_response_by_handler_ptr(ptr: usize) -> Option<ResponseMeta> {
81    RESPONSE_REGISTRY
82        .lock()
83        .ok()
84        .and_then(|m| m.get(&ptr).cloned())
85}
86
87pub fn list_registered_json_types() -> Vec<&'static str> {
88    let map = RESPONSE_REGISTRY.lock().ok();
89    let mut out = Vec::new();
90    if let Some(map) = map {
91        for meta in map.values() {
92            if let ResponseMeta::Json { type_name } = meta
93                && !out.contains(type_name)
94            {
95                out.push(*type_name);
96            }
97        }
98    }
99    out
100}
101
102// ====== 额外响应(非 200)注册 ======
103
104/// 额外的 HTTP 响应描述(如 400、401、404 等)
105#[derive(Clone, Debug)]
106pub struct ExtraResponse {
107    pub status: u16,
108    pub description: String,
109}
110
111static EXTRA_RESPONSE_REGISTRY: Lazy<Mutex<HashMap<usize, Vec<ExtraResponse>>>> =
112    Lazy::new(|| Mutex::new(HashMap::new()));
113
114pub fn register_extra_response_by_ptr(ptr: usize, status: u16, description: &str) {
115    let mut map = EXTRA_RESPONSE_REGISTRY
116        .lock()
117        .expect("extra response registry poisoned");
118    map.entry(ptr).or_default().push(ExtraResponse {
119        status,
120        description: description.to_string(),
121    });
122}
123
124pub(crate) fn lookup_extra_responses_by_handler_ptr(ptr: usize) -> Option<Vec<ExtraResponse>> {
125    EXTRA_RESPONSE_REGISTRY
126        .lock()
127        .ok()
128        .and_then(|m| m.get(&ptr).cloned())
129}
130
131// ====== 请求元信息注册 ======
132
133/// 请求参数/请求体元信息
134#[derive(Clone, Debug)]
135pub enum RequestMeta {
136    /// JSON 请求体(对应 Json<T> 提取器)
137    JsonBody { type_name: &'static str },
138    /// 表单请求体(对应 Form<T> 提取器)
139    FormBody { type_name: &'static str },
140    /// 查询参数(对应 Query<T> 提取器)
141    QueryParams { type_name: &'static str },
142}
143
144static REQUEST_REGISTRY: Lazy<Mutex<HashMap<usize, Vec<RequestMeta>>>> =
145    Lazy::new(|| Mutex::new(HashMap::new()));
146
147pub fn register_request_by_ptr(ptr: usize, meta: RequestMeta) {
148    let mut map = REQUEST_REGISTRY.lock().expect("request registry poisoned");
149    map.entry(ptr).or_default().push(meta);
150}
151
152pub(crate) fn lookup_request_by_handler_ptr(ptr: usize) -> Option<Vec<RequestMeta>> {
153    REQUEST_REGISTRY
154        .lock()
155        .ok()
156        .and_then(|m| m.get(&ptr).cloned())
157}
158
159// ====== ToSchema 完整 schema 注册 ======
160type SchemaRegFn = fn(&mut Components);
161static SCHEMA_REGISTRY: Lazy<Mutex<Vec<SchemaRegFn>>> = Lazy::new(|| Mutex::new(Vec::new()));
162
163pub fn register_schema_for<T>()
164where
165    T: crate::ToSchema + ::utoipa::PartialSchema + 'static,
166{
167    fn add_impl<U: crate::ToSchema + ::utoipa::PartialSchema>(components: &mut Components) {
168        let mut refs: Vec<(
169            String,
170            ::utoipa::openapi::RefOr<::utoipa::openapi::schema::Schema>,
171        )> = Vec::new();
172        <U as crate::ToSchema>::schemas(&mut refs);
173        for (name, schema) in refs {
174            components.schemas.entry(name).or_insert(schema);
175        }
176        let name = <U as crate::ToSchema>::name().into_owned();
177        let schema = <U as ::utoipa::PartialSchema>::schema();
178        components.schemas.entry(name).or_insert(schema);
179    }
180    let mut reg = SCHEMA_REGISTRY.lock().expect("schema registry poisoned");
181    reg.push(add_impl::<T> as SchemaRegFn);
182}
183
184pub fn apply_registered_schemas(openapi: &mut OpenApi) {
185    let mut components = openapi
186        .components
187        .clone()
188        .unwrap_or_else(|| ComponentsBuilder::new().build());
189    if let Ok(reg) = SCHEMA_REGISTRY.lock() {
190        for f in reg.iter() {
191            f(&mut components);
192        }
193    }
194    openapi.components = Some(components);
195}
196
197/// 路由文档标注扩展:在完成 handler 挂载后,追加文档说明
198pub trait RouteDocMarkExt {
199    fn doc(self, method: Method, summary: &str, description: &str) -> Self;
200}
201
202/// 便捷构造:将基于 Request 的处理函数包装为 `Arc<dyn Handler>` 并注册文档
203pub fn handler_with_doc<F, Fut, T>(f: F, summary: &str, description: &str) -> Arc<dyn Handler>
204where
205    F: Fn(SilentRequest) -> Fut + Send + Sync + 'static,
206    Fut: core::future::Future<Output = SilentResult<T>> + Send + 'static,
207    T: Into<SilentResponse> + Send + 'static,
208{
209    let handler = Arc::new(HandlerWrapper::new(f));
210    let ptr = Arc::as_ptr(&handler) as *const () as usize;
211    register_doc_by_ptr(ptr, Some(summary), Some(description));
212    handler
213}
214
215impl RouteDocMarkExt for Route {
216    fn doc(self, method: Method, summary: &str, description: &str) -> Self {
217        if let Some(handler) = self.handler.get(&method).cloned() {
218            let ptr = Arc::as_ptr(&handler) as *const () as usize;
219            register_doc_by_ptr(ptr, Some(summary), Some(description));
220        }
221        self
222    }
223}
224
225/// 便捷追加:同时挂载处理器并标注文档
226pub trait RouteDocAppendExt {
227    fn get_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
228    fn post_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
229    fn put_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
230    fn delete_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
231    fn patch_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
232    fn options_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
233}
234
235impl RouteDocAppendExt for Route {
236    fn get_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
237        let ptr = Arc::as_ptr(&handler) as *const () as usize;
238        register_doc_by_ptr(ptr, Some(summary), Some(description));
239        <Route as HandlerGetter>::handler(self, Method::GET, handler)
240    }
241
242    fn post_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
243        let ptr = Arc::as_ptr(&handler) as *const () as usize;
244        register_doc_by_ptr(ptr, Some(summary), Some(description));
245        <Route as HandlerGetter>::handler(self, Method::POST, handler)
246    }
247
248    fn put_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
249        let ptr = Arc::as_ptr(&handler) as *const () as usize;
250        register_doc_by_ptr(ptr, Some(summary), Some(description));
251        <Route as HandlerGetter>::handler(self, Method::PUT, handler)
252    }
253
254    fn delete_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
255        let ptr = Arc::as_ptr(&handler) as *const () as usize;
256        register_doc_by_ptr(ptr, Some(summary), Some(description));
257        <Route as HandlerGetter>::handler(self, Method::DELETE, handler)
258    }
259
260    fn patch_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
261        let ptr = Arc::as_ptr(&handler) as *const () as usize;
262        register_doc_by_ptr(ptr, Some(summary), Some(description));
263        <Route as HandlerGetter>::handler(self, Method::PATCH, handler)
264    }
265
266    fn options_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
267        let ptr = Arc::as_ptr(&handler) as *const () as usize;
268        register_doc_by_ptr(ptr, Some(summary), Some(description));
269        <Route as HandlerGetter>::handler(self, Method::OPTIONS, handler)
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use serde::Serialize;
277    use utoipa::ToSchema;
278
279    async fn ok_handler(_req: SilentRequest) -> SilentResult<SilentResponse> {
280        Ok(SilentResponse::text("ok"))
281    }
282
283    #[test]
284    fn test_register_and_lookup_doc() {
285        let handler = Arc::new(HandlerWrapper::new(|_req: SilentRequest| async move {
286            Ok::<_, silent::SilentError>(SilentResponse::text("doc"))
287        }));
288        let ptr = Arc::as_ptr(&handler) as *const () as usize;
289        register_doc_by_ptr(ptr, Some("summary"), Some("desc"));
290        let got = lookup_doc_by_handler_ptr(ptr).expect("doc meta");
291        assert_eq!(got.summary.as_deref(), Some("summary"));
292        assert_eq!(got.description.as_deref(), Some("desc"));
293    }
294
295    #[test]
296    fn test_register_and_lookup_response() {
297        let handler = Arc::new(HandlerWrapper::new(ok_handler));
298        let ptr = Arc::as_ptr(&handler) as *const () as usize;
299        register_response_by_ptr(ptr, ResponseMeta::TextPlain);
300        let got = lookup_response_by_handler_ptr(ptr).expect("resp meta");
301        matches!(got, ResponseMeta::TextPlain);
302    }
303
304    #[test]
305    fn test_list_registered_json_types() {
306        let h1 = Arc::new(HandlerWrapper::new(ok_handler));
307        let h2 = Arc::new(HandlerWrapper::new(ok_handler));
308        let p1 = Arc::as_ptr(&h1) as *const () as usize;
309        let p2 = Arc::as_ptr(&h2) as *const () as usize;
310        register_response_by_ptr(p1, ResponseMeta::Json { type_name: "User" });
311        register_response_by_ptr(p2, ResponseMeta::Json { type_name: "User" });
312        let list = list_registered_json_types();
313        assert!(list.contains(&"User"));
314        assert_eq!(list.len(), 1);
315    }
316
317    #[derive(Serialize, ToSchema)]
318    struct FooSchema {
319        id: i32,
320        name: String,
321    }
322
323    #[test]
324    fn test_register_schema_and_apply() {
325        register_schema_for::<FooSchema>();
326        let mut openapi = crate::OpenApiDoc::new("T", "1").into_openapi();
327        apply_registered_schemas(&mut openapi);
328        let components = openapi.components.expect("components");
329        assert!(components.schemas.contains_key("FooSchema"));
330    }
331
332    // ====== 枚举变体文档测试 ======
333
334    #[derive(Serialize, ToSchema)]
335    #[allow(dead_code)]
336    enum ApiResponse {
337        Success { data: String },
338        Error { code: i32, message: String },
339    }
340
341    #[test]
342    fn test_register_enum_schema() {
343        register_schema_for::<ApiResponse>();
344        let mut openapi = crate::OpenApiDoc::new("T", "1").into_openapi();
345        apply_registered_schemas(&mut openapi);
346        let components = openapi.components.expect("components");
347        assert!(components.schemas.contains_key("ApiResponse"));
348    }
349
350    #[derive(Serialize, ToSchema)]
351    #[allow(dead_code)]
352    enum Status {
353        Active,
354        Inactive,
355        Pending,
356    }
357
358    #[test]
359    fn test_register_unit_enum_schema() {
360        register_schema_for::<Status>();
361        let mut openapi = crate::OpenApiDoc::new("T", "1").into_openapi();
362        apply_registered_schemas(&mut openapi);
363        let components = openapi.components.expect("components");
364        assert!(components.schemas.contains_key("Status"));
365    }
366
367    #[derive(Serialize, ToSchema)]
368    struct NestedData {
369        value: i32,
370    }
371
372    #[derive(Serialize, ToSchema)]
373    #[allow(dead_code)]
374    enum ComplexEnum {
375        WithStruct(NestedData),
376        WithString(String),
377        Empty,
378    }
379
380    #[test]
381    fn test_register_enum_with_nested_schemas() {
382        register_schema_for::<ComplexEnum>();
383        let mut openapi = crate::OpenApiDoc::new("T", "1").into_openapi();
384        apply_registered_schemas(&mut openapi);
385        let components = openapi.components.expect("components");
386        assert!(components.schemas.contains_key("ComplexEnum"));
387        // 嵌套的 NestedData 也应被注册
388        assert!(components.schemas.contains_key("NestedData"));
389    }
390
391    #[test]
392    fn test_register_request_and_lookup() {
393        let handler = Arc::new(HandlerWrapper::new(ok_handler));
394        let ptr = Arc::as_ptr(&handler) as *const () as usize;
395        register_request_by_ptr(ptr, RequestMeta::JsonBody { type_name: "User" });
396        register_request_by_ptr(
397            ptr,
398            RequestMeta::QueryParams {
399                type_name: "Filter",
400            },
401        );
402        let got = lookup_request_by_handler_ptr(ptr).expect("request meta");
403        assert_eq!(got.len(), 2);
404        assert!(matches!(
405            &got[0],
406            RequestMeta::JsonBody { type_name: "User" }
407        ));
408        assert!(matches!(
409            &got[1],
410            RequestMeta::QueryParams {
411                type_name: "Filter"
412            }
413        ));
414    }
415}