1use std::any::TypeId;
2use std::collections::{BTreeSet, HashMap};
3use std::sync::{LazyLock, RwLock};
4
5use salvo_core::Router;
6use salvo_core::http::Method;
7use salvo_core::routing::FilterInfo;
8
9use crate::SecurityRequirement;
10use crate::path::PathItemType;
11
12fn normalize_oapi_path(path: &str) -> String {
13 let mut normalized = String::with_capacity(path.len());
14 let mut chars = path.char_indices().peekable();
15
16 while let Some((start, ch)) = chars.next() {
17 if ch != '{' {
18 normalized.push(ch);
19 continue;
20 }
21 if chars.peek().map(|(_, next)| *next) == Some('{') {
23 normalized.push('{');
24 normalized.push('{');
25 chars.next();
26 continue;
27 }
28
29 let content_start = start + ch.len_utf8();
30 let mut braces_depth = 0usize;
31 let mut escaping = false;
32 let mut param_end = None;
33
34 for (idx, current) in chars.by_ref() {
35 if escaping {
36 escaping = false;
37 continue;
38 }
39 match current {
40 '\\' => escaping = true,
41 '{' => braces_depth += 1,
42 '}' => {
43 if braces_depth == 0 {
44 param_end = Some(idx);
45 break;
46 }
47 braces_depth -= 1;
48 }
49 _ => {}
50 }
51 }
52
53 if let Some(param_end) = param_end {
54 let Some(content) = path.get(content_start..param_end) else {
55 break;
56 };
57 if let Some(name_end) = content.find([':', '|']) {
58 normalized.push('{');
59 let Some(name) = content.get(..name_end) else {
60 break;
61 };
62 normalized.push_str(name);
63 normalized.push('}');
64 } else {
65 normalized.push('{');
66 normalized.push_str(content);
67 normalized.push('}');
68 }
69 } else {
70 if let Some(rest) = path.get(start..) {
71 normalized.push_str(rest);
72 }
73 break;
74 }
75 }
76 normalized
77}
78
79#[derive(Debug, Default)]
80pub(crate) struct NormNode {
81 pub(crate) handler_type_id: Option<TypeId>,
83 pub(crate) handler_type_name: Option<&'static str>,
84 pub(crate) method: Option<PathItemType>,
85 pub(crate) path: Option<String>,
86 pub(crate) children: Vec<Self>,
87 pub(crate) metadata: Metadata,
88}
89
90impl NormNode {
91 pub(crate) fn new(router: &Router, inherited_metadata: Metadata) -> Self {
92 let mut node = Self {
93 metadata: inherited_metadata,
95 ..Self::default()
96 };
97 let registry = METADATA_REGISTRY
98 .read()
99 .expect("failed to lock METADATA_REGISTRY for read");
100 if let Some(metadata) = registry.get(&router.id) {
101 node.metadata.tags.extend(metadata.tags.iter().cloned());
102 node.metadata
103 .securities
104 .extend(metadata.securities.iter().cloned());
105 }
106
107 for filter in router.filters() {
108 match filter.info() {
109 FilterInfo::Path(path) => {
110 node.path = Some(normalize_oapi_path(&path));
111 }
112 FilterInfo::Method(method) => {
113 let item = match method {
124 Method::GET => Some(PathItemType::Get),
125 Method::POST => Some(PathItemType::Post),
126 Method::PUT => Some(PathItemType::Put),
127 Method::DELETE => Some(PathItemType::Delete),
128 Method::HEAD => Some(PathItemType::Head),
129 Method::OPTIONS => Some(PathItemType::Options),
130 Method::TRACE => Some(PathItemType::Trace),
131 Method::PATCH => Some(PathItemType::Patch),
132 _ => None,
133 };
134 if item.is_some() {
135 node.method = item;
136 } else if method == Method::CONNECT {
137 tracing::warn!(
138 "HTTP CONNECT has no OpenAPI 3.1 mapping; the route will be \
139 omitted from the generated document"
140 );
141 }
142 }
143 _ => {}
146 }
147 }
148 node.handler_type_id = router.goal.as_ref().map(|h| h.type_id());
149 node.handler_type_name = router.goal.as_ref().map(|h| h.type_name());
150 let routers = router.routers();
151 if !routers.is_empty() {
152 for router in routers {
153 node.children.push(Self::new(router, node.metadata.clone()));
154 }
155 }
156 node
157 }
158}
159
160type MetadataMap = RwLock<HashMap<usize, Metadata>>;
162static METADATA_REGISTRY: LazyLock<MetadataMap> = LazyLock::new(MetadataMap::default);
163
164pub trait RouterExt {
166 #[must_use]
170 fn oapi_security(self, security: SecurityRequirement) -> Self;
171
172 #[must_use]
176 fn oapi_securities<I>(self, security: I) -> Self
177 where
178 I: IntoIterator<Item = SecurityRequirement>;
179
180 #[must_use]
184 fn oapi_tag(self, tag: impl Into<String>) -> Self;
185
186 #[must_use]
190 fn oapi_tags<I, V>(self, tags: I) -> Self
191 where
192 I: IntoIterator<Item = V>,
193 V: Into<String>;
194}
195
196impl RouterExt for Router {
197 fn oapi_security(self, security: SecurityRequirement) -> Self {
198 let mut guard = METADATA_REGISTRY
199 .write()
200 .expect("failed to lock METADATA_REGISTRY for write");
201 let metadata = guard.entry(self.id).or_default();
202 metadata.securities.push(security);
203 self
204 }
205 fn oapi_securities<I>(self, iter: I) -> Self
206 where
207 I: IntoIterator<Item = SecurityRequirement>,
208 {
209 let mut guard = METADATA_REGISTRY
210 .write()
211 .expect("failed to lock METADATA_REGISTRY for write");
212 let metadata = guard.entry(self.id).or_default();
213 metadata.securities.extend(iter);
214 self
215 }
216 fn oapi_tag(self, tag: impl Into<String>) -> Self {
217 let mut guard = METADATA_REGISTRY
218 .write()
219 .expect("failed to lock METADATA_REGISTRY for write");
220 let metadata = guard.entry(self.id).or_default();
221 metadata.tags.insert(tag.into());
222 self
223 }
224 fn oapi_tags<I, V>(self, iter: I) -> Self
225 where
226 I: IntoIterator<Item = V>,
227 V: Into<String>,
228 {
229 let mut guard = METADATA_REGISTRY
230 .write()
231 .expect("failed to lock METADATA_REGISTRY for write");
232 let metadata = guard.entry(self.id).or_default();
233 metadata.tags.extend(iter.into_iter().map(Into::into));
234 self
235 }
236}
237
238#[non_exhaustive]
239#[derive(Default, Clone, Debug)]
240pub(crate) struct Metadata {
241 pub(crate) tags: BTreeSet<String>,
242 pub(crate) securities: Vec<SecurityRequirement>,
243}
244
245#[cfg(test)]
246mod tests {
247 use super::normalize_oapi_path;
248
249 #[test]
250 fn normalize_braced_path_constraints() {
251 assert_eq!(normalize_oapi_path("/posts/{id}"), "/posts/{id}");
252 assert_eq!(normalize_oapi_path("/posts/{id:num}"), "/posts/{id}");
253 assert_eq!(
254 normalize_oapi_path("/posts/{id:num(3..=10)}"),
255 "/posts/{id}"
256 );
257 assert_eq!(normalize_oapi_path(r"/posts/{id|\d+}"), "/posts/{id}");
258 assert_eq!(normalize_oapi_path("/posts/{id|[a-z]{2}}"), "/posts/{id}");
259 assert_eq!(
260 normalize_oapi_path("/posts/article_{id:num}"),
261 "/posts/article_{id}"
262 );
263 }
264}