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, Clone, PartialEq, Eq)]
81pub(crate) enum OperationSlot {
82 Standard(PathItemType),
84 Additional(String),
87}
88
89#[derive(Debug, Default)]
90pub(crate) struct NormNode {
91 pub(crate) handler_type_id: Option<TypeId>,
93 pub(crate) handler_type_name: Option<&'static str>,
94 pub(crate) method: Option<OperationSlot>,
95 pub(crate) path: Option<String>,
96 pub(crate) children: Vec<Self>,
97 pub(crate) metadata: Metadata,
98}
99
100impl NormNode {
101 pub(crate) fn new(router: &Router, inherited_metadata: Metadata) -> Self {
102 let mut node = Self {
103 metadata: inherited_metadata,
105 ..Self::default()
106 };
107 let registry = METADATA_REGISTRY
108 .read()
109 .expect("failed to lock METADATA_REGISTRY for read");
110 if let Some(metadata) = registry.get(&router.id) {
111 node.metadata.tags.extend(metadata.tags.iter().cloned());
112 node.metadata
113 .securities
114 .extend(metadata.securities.iter().cloned());
115 }
116
117 for filter in router.filters() {
118 match filter.info() {
119 FilterInfo::Path(path) => {
120 node.path = Some(normalize_oapi_path(&path));
121 }
122 FilterInfo::Method(method) => {
123 let item = match method {
129 Method::GET => OperationSlot::Standard(PathItemType::Get),
130 Method::POST => OperationSlot::Standard(PathItemType::Post),
131 Method::PUT => OperationSlot::Standard(PathItemType::Put),
132 Method::DELETE => OperationSlot::Standard(PathItemType::Delete),
133 Method::HEAD => OperationSlot::Standard(PathItemType::Head),
134 Method::OPTIONS => OperationSlot::Standard(PathItemType::Options),
135 Method::TRACE => OperationSlot::Standard(PathItemType::Trace),
136 Method::PATCH => OperationSlot::Standard(PathItemType::Patch),
137 Method::QUERY => OperationSlot::Standard(PathItemType::Query),
138 other => OperationSlot::Additional(other.as_str().to_owned()),
139 };
140 if matches!(item, OperationSlot::Standard(_))
145 || !matches!(node.method, Some(OperationSlot::Standard(_)))
146 {
147 node.method = Some(item);
148 }
149 }
150 _ => {}
153 }
154 }
155 node.handler_type_id = router.goal.as_ref().map(|h| h.type_id());
156 node.handler_type_name = router.goal.as_ref().map(|h| h.type_name());
157 let routers = router.routers();
158 if !routers.is_empty() {
159 for router in routers {
160 node.children.push(Self::new(router, node.metadata.clone()));
161 }
162 }
163 node
164 }
165}
166
167type MetadataMap = RwLock<HashMap<usize, Metadata>>;
169static METADATA_REGISTRY: LazyLock<MetadataMap> = LazyLock::new(MetadataMap::default);
170
171pub trait RouterExt {
173 #[must_use]
177 fn oapi_security(self, security: SecurityRequirement) -> Self;
178
179 #[must_use]
183 fn oapi_securities<I>(self, security: I) -> Self
184 where
185 I: IntoIterator<Item = SecurityRequirement>;
186
187 #[must_use]
191 fn oapi_tag(self, tag: impl Into<String>) -> Self;
192
193 #[must_use]
197 fn oapi_tags<I, V>(self, tags: I) -> Self
198 where
199 I: IntoIterator<Item = V>,
200 V: Into<String>;
201}
202
203impl RouterExt for Router {
204 fn oapi_security(self, security: SecurityRequirement) -> Self {
205 let mut guard = METADATA_REGISTRY
206 .write()
207 .expect("failed to lock METADATA_REGISTRY for write");
208 let metadata = guard.entry(self.id).or_default();
209 metadata.securities.push(security);
210 self
211 }
212 fn oapi_securities<I>(self, iter: I) -> Self
213 where
214 I: IntoIterator<Item = SecurityRequirement>,
215 {
216 let mut guard = METADATA_REGISTRY
217 .write()
218 .expect("failed to lock METADATA_REGISTRY for write");
219 let metadata = guard.entry(self.id).or_default();
220 metadata.securities.extend(iter);
221 self
222 }
223 fn oapi_tag(self, tag: impl Into<String>) -> Self {
224 let mut guard = METADATA_REGISTRY
225 .write()
226 .expect("failed to lock METADATA_REGISTRY for write");
227 let metadata = guard.entry(self.id).or_default();
228 metadata.tags.insert(tag.into());
229 self
230 }
231 fn oapi_tags<I, V>(self, iter: I) -> Self
232 where
233 I: IntoIterator<Item = V>,
234 V: Into<String>,
235 {
236 let mut guard = METADATA_REGISTRY
237 .write()
238 .expect("failed to lock METADATA_REGISTRY for write");
239 let metadata = guard.entry(self.id).or_default();
240 metadata.tags.extend(iter.into_iter().map(Into::into));
241 self
242 }
243}
244
245#[non_exhaustive]
246#[derive(Default, Clone, Debug)]
247pub(crate) struct Metadata {
248 pub(crate) tags: BTreeSet<String>,
249 pub(crate) securities: Vec<SecurityRequirement>,
250}
251
252#[cfg(test)]
253mod tests {
254 use super::normalize_oapi_path;
255
256 #[test]
257 fn normalize_braced_path_constraints() {
258 assert_eq!(normalize_oapi_path("/posts/{id}"), "/posts/{id}");
259 assert_eq!(normalize_oapi_path("/posts/{id:num}"), "/posts/{id}");
260 assert_eq!(
261 normalize_oapi_path("/posts/{id:num(3..=10)}"),
262 "/posts/{id}"
263 );
264 assert_eq!(normalize_oapi_path(r"/posts/{id|\d+}"), "/posts/{id}");
265 assert_eq!(normalize_oapi_path("/posts/{id|[a-z]{2}}"), "/posts/{id}");
266 assert_eq!(
267 normalize_oapi_path("/posts/article_{id:num}"),
268 "/posts/article_{id}"
269 );
270 }
271}