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}
20
21static DOC_REGISTRY: Lazy<Mutex<HashMap<usize, DocMeta>>> =
22    Lazy::new(|| Mutex::new(HashMap::new()));
23
24pub fn register_doc_by_ptr(ptr: usize, summary: Option<&str>, description: Option<&str>) {
25    let mut map = DOC_REGISTRY.lock().expect("doc registry poisoned");
26    map.insert(
27        ptr,
28        DocMeta {
29            summary: summary.map(|s| s.to_string()),
30            description: description.map(|s| s.to_string()),
31        },
32    );
33}
34
35pub(crate) fn lookup_doc_by_handler_ptr(ptr: usize) -> Option<DocMeta> {
36    DOC_REGISTRY.lock().ok().and_then(|m| m.get(&ptr).cloned())
37}
38
39/// 响应类型元信息
40#[derive(Clone, Debug)]
41pub enum ResponseMeta {
42    TextPlain,
43    Json { type_name: &'static str },
44}
45
46static RESPONSE_REGISTRY: Lazy<Mutex<HashMap<usize, ResponseMeta>>> =
47    Lazy::new(|| Mutex::new(HashMap::new()));
48
49pub fn register_response_by_ptr(ptr: usize, meta: ResponseMeta) {
50    let mut map = RESPONSE_REGISTRY
51        .lock()
52        .expect("response registry poisoned");
53    map.insert(ptr, meta);
54}
55
56pub(crate) fn lookup_response_by_handler_ptr(ptr: usize) -> Option<ResponseMeta> {
57    RESPONSE_REGISTRY
58        .lock()
59        .ok()
60        .and_then(|m| m.get(&ptr).cloned())
61}
62
63pub fn list_registered_json_types() -> Vec<&'static str> {
64    let map = RESPONSE_REGISTRY.lock().ok();
65    let mut out = Vec::new();
66    if let Some(map) = map {
67        for meta in map.values() {
68            if let ResponseMeta::Json { type_name } = meta
69                && !out.contains(type_name)
70            {
71                out.push(*type_name);
72            }
73        }
74    }
75    out
76}
77
78// ====== ToSchema 完整 schema 注册 ======
79type SchemaRegFn = fn(&mut Components);
80static SCHEMA_REGISTRY: Lazy<Mutex<Vec<SchemaRegFn>>> = Lazy::new(|| Mutex::new(Vec::new()));
81
82pub fn register_schema_for<T>()
83where
84    T: crate::ToSchema + ::utoipa::PartialSchema + 'static,
85{
86    fn add_impl<U: crate::ToSchema + ::utoipa::PartialSchema>(components: &mut Components) {
87        let mut refs: Vec<(
88            String,
89            ::utoipa::openapi::RefOr<::utoipa::openapi::schema::Schema>,
90        )> = Vec::new();
91        <U as crate::ToSchema>::schemas(&mut refs);
92        for (name, schema) in refs {
93            components.schemas.entry(name).or_insert(schema);
94        }
95        let name = <U as crate::ToSchema>::name().into_owned();
96        let schema = <U as ::utoipa::PartialSchema>::schema();
97        components.schemas.entry(name).or_insert(schema);
98    }
99    let mut reg = SCHEMA_REGISTRY.lock().expect("schema registry poisoned");
100    reg.push(add_impl::<T> as SchemaRegFn);
101}
102
103pub fn apply_registered_schemas(openapi: &mut OpenApi) {
104    let mut components = openapi
105        .components
106        .clone()
107        .unwrap_or_else(|| ComponentsBuilder::new().build());
108    if let Ok(reg) = SCHEMA_REGISTRY.lock() {
109        for f in reg.iter() {
110            f(&mut components);
111        }
112    }
113    openapi.components = Some(components);
114}
115
116/// 路由文档标注扩展:在完成 handler 挂载后,追加文档说明
117pub trait RouteDocMarkExt {
118    fn doc(self, method: Method, summary: &str, description: &str) -> Self;
119}
120
121/// 便捷构造:将基于 Request 的处理函数包装为 `Arc<dyn Handler>` 并注册文档
122pub fn handler_with_doc<F, Fut, T>(f: F, summary: &str, description: &str) -> Arc<dyn Handler>
123where
124    F: Fn(SilentRequest) -> Fut + Send + Sync + 'static,
125    Fut: core::future::Future<Output = SilentResult<T>> + Send + 'static,
126    T: Into<SilentResponse> + Send + 'static,
127{
128    let handler = Arc::new(HandlerWrapper::new(f));
129    let ptr = Arc::as_ptr(&handler) as *const () as usize;
130    register_doc_by_ptr(ptr, Some(summary), Some(description));
131    handler
132}
133
134impl RouteDocMarkExt for Route {
135    fn doc(self, method: Method, summary: &str, description: &str) -> Self {
136        if let Some(handler) = self.handler.get(&method).cloned() {
137            let ptr = Arc::as_ptr(&handler) as *const () as usize;
138            register_doc_by_ptr(ptr, Some(summary), Some(description));
139        }
140        self
141    }
142}
143
144/// 便捷追加:同时挂载处理器并标注文档
145pub trait RouteDocAppendExt {
146    fn get_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
147    fn post_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
148    fn put_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
149    fn delete_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
150    fn patch_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
151    fn options_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self;
152}
153
154impl RouteDocAppendExt for Route {
155    fn get_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
156        let ptr = Arc::as_ptr(&handler) as *const () as usize;
157        register_doc_by_ptr(ptr, Some(summary), Some(description));
158        <Route as HandlerGetter>::handler(self, Method::GET, handler)
159    }
160
161    fn post_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
162        let ptr = Arc::as_ptr(&handler) as *const () as usize;
163        register_doc_by_ptr(ptr, Some(summary), Some(description));
164        <Route as HandlerGetter>::handler(self, Method::POST, handler)
165    }
166
167    fn put_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
168        let ptr = Arc::as_ptr(&handler) as *const () as usize;
169        register_doc_by_ptr(ptr, Some(summary), Some(description));
170        <Route as HandlerGetter>::handler(self, Method::PUT, handler)
171    }
172
173    fn delete_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
174        let ptr = Arc::as_ptr(&handler) as *const () as usize;
175        register_doc_by_ptr(ptr, Some(summary), Some(description));
176        <Route as HandlerGetter>::handler(self, Method::DELETE, handler)
177    }
178
179    fn patch_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
180        let ptr = Arc::as_ptr(&handler) as *const () as usize;
181        register_doc_by_ptr(ptr, Some(summary), Some(description));
182        <Route as HandlerGetter>::handler(self, Method::PATCH, handler)
183    }
184
185    fn options_with_doc(self, handler: Arc<dyn Handler>, summary: &str, description: &str) -> Self {
186        let ptr = Arc::as_ptr(&handler) as *const () as usize;
187        register_doc_by_ptr(ptr, Some(summary), Some(description));
188        <Route as HandlerGetter>::handler(self, Method::OPTIONS, handler)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use serde::Serialize;
196    use utoipa::ToSchema;
197
198    async fn ok_handler(_req: SilentRequest) -> SilentResult<SilentResponse> {
199        Ok(SilentResponse::text("ok"))
200    }
201
202    #[test]
203    fn test_register_and_lookup_doc() {
204        let handler = Arc::new(HandlerWrapper::new(|_req: SilentRequest| async move {
205            Ok::<_, silent::SilentError>(SilentResponse::text("doc"))
206        }));
207        let ptr = Arc::as_ptr(&handler) as *const () as usize;
208        register_doc_by_ptr(ptr, Some("summary"), Some("desc"));
209        let got = lookup_doc_by_handler_ptr(ptr).expect("doc meta");
210        assert_eq!(got.summary.as_deref(), Some("summary"));
211        assert_eq!(got.description.as_deref(), Some("desc"));
212    }
213
214    #[test]
215    fn test_register_and_lookup_response() {
216        let handler = Arc::new(HandlerWrapper::new(ok_handler));
217        let ptr = Arc::as_ptr(&handler) as *const () as usize;
218        register_response_by_ptr(ptr, ResponseMeta::TextPlain);
219        let got = lookup_response_by_handler_ptr(ptr).expect("resp meta");
220        matches!(got, ResponseMeta::TextPlain);
221    }
222
223    #[test]
224    fn test_list_registered_json_types() {
225        let h1 = Arc::new(HandlerWrapper::new(ok_handler));
226        let h2 = Arc::new(HandlerWrapper::new(ok_handler));
227        let p1 = Arc::as_ptr(&h1) as *const () as usize;
228        let p2 = Arc::as_ptr(&h2) as *const () as usize;
229        register_response_by_ptr(p1, ResponseMeta::Json { type_name: "User" });
230        register_response_by_ptr(p2, ResponseMeta::Json { type_name: "User" });
231        let list = list_registered_json_types();
232        assert!(list.contains(&"User"));
233        assert_eq!(list.len(), 1);
234    }
235
236    #[derive(Serialize, ToSchema)]
237    struct FooSchema {
238        id: i32,
239        name: String,
240    }
241
242    #[test]
243    fn test_register_schema_and_apply() {
244        register_schema_for::<FooSchema>();
245        let mut openapi = crate::OpenApiDoc::new("T", "1").into_openapi();
246        apply_registered_schemas(&mut openapi);
247        let components = openapi.components.expect("components");
248        assert!(components.schemas.contains_key("FooSchema"));
249    }
250}