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#[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
39pub 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#[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#[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#[derive(Clone, Debug)]
135pub enum RequestMeta {
136 JsonBody { type_name: &'static str },
138 FormBody { type_name: &'static str },
140 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
159type 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
197pub trait RouteDocMarkExt {
199 fn doc(self, method: Method, summary: &str, description: &str) -> Self;
200}
201
202pub 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
225pub 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 #[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 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}