1use std::collections::HashMap;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11
12pub trait WebSocketEndpointInfo {
19 fn path() -> &'static str;
21 fn name() -> Option<&'static str>;
23}
24
25pub struct WebSocketEndpointMetadata {
29 pub path: &'static str,
31 pub name: &'static str,
33 pub fn_name: &'static str,
35 pub module_path: &'static str,
37}
38
39inventory::collect!(WebSocketEndpointMetadata);
40
41pub fn substitute_ws_params(path: &str, params: &[(&str, &str)]) -> String {
45 let mut result = path.to_string();
46 for (name, value) in params {
47 result = result.replace(&format!("{{{}}}", name), value);
48 }
49 result
50}
51
52pub type RouteResult = Result<(), RouteError>;
56
57#[derive(Debug, thiserror::Error)]
59pub enum RouteError {
60 #[error("Route not found: {0}")]
62 NotFound(String),
63 #[error("Route already exists: {0}")]
65 AlreadyExists(String),
66 #[error("Invalid route pattern: {0}")]
68 InvalidPattern(String),
69}
70
71#[derive(Debug, Clone)]
73pub struct WebSocketRoute {
74 path: String,
75 name: Option<String>,
76 metadata: HashMap<String, String>,
77}
78
79impl WebSocketRoute {
80 pub fn new(path: String, name: Option<String>) -> Self {
82 Self {
83 path,
84 name,
85 metadata: HashMap::new(),
86 }
87 }
88
89 pub fn path(&self) -> &str {
91 &self.path
92 }
93
94 pub fn name(&self) -> Option<&str> {
96 self.name.as_deref()
97 }
98
99 pub fn with_metadata(mut self, key: String, value: String) -> Self {
101 self.metadata.insert(key, value);
102 self
103 }
104
105 pub fn get_metadata(&self, key: &str) -> Option<&String> {
107 self.metadata.get(key)
108 }
109}
110
111#[derive(Clone)]
120pub struct WebSocketRouter {
121 routes: Arc<RwLock<HashMap<String, WebSocketRoute>>>,
122 names: Arc<RwLock<HashMap<String, String>>>,
123 pending_consumers: Vec<WebSocketRoute>,
125 namespace: Option<String>,
127}
128
129impl WebSocketRouter {
130 pub fn new() -> Self {
132 Self {
133 routes: Arc::new(RwLock::new(HashMap::new())),
134 names: Arc::new(RwLock::new(HashMap::new())),
135 pending_consumers: Vec::new(),
136 namespace: None,
137 }
138 }
139
140 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
147 self.namespace = Some(namespace.into());
148 self
149 }
150
151 pub fn namespace(&self) -> Option<&str> {
155 self.namespace.as_deref()
156 }
157
158 pub fn consumer<C, F>(mut self, _f: F) -> Self
163 where
164 F: Fn() -> C,
165 C: WebSocketEndpointInfo + 'static,
166 {
167 self.pending_consumers.push(WebSocketRoute::new(
168 C::path().to_string(),
169 C::name().map(|s| s.to_string()),
170 ));
171 self
172 }
173
174 pub fn find_pending(&self, name: &str) -> Option<&WebSocketRoute> {
176 self.pending_consumers
177 .iter()
178 .find(|r| r.name() == Some(name))
179 }
180
181 pub fn reverse(&self, name: &str, params: &[(&str, &str)]) -> Option<String> {
183 self.pending_consumers
184 .iter()
185 .find(|r| r.name() == Some(name))
186 .map(|r| substitute_ws_params(r.path(), params))
187 }
188
189 pub async fn register_route(&mut self, route: WebSocketRoute) -> RouteResult {
191 let mut routes = self.routes.write().await;
192 if routes.contains_key(&route.path) {
193 return Err(RouteError::AlreadyExists(route.path.clone()));
194 }
195 if let Some(name) = &route.name {
196 let mut names = self.names.write().await;
197 names.insert(name.clone(), route.path.clone());
198 }
199 routes.insert(route.path.clone(), route);
200 Ok(())
201 }
202
203 pub async fn find_route(&self, path: &str) -> Option<WebSocketRoute> {
205 let routes = self.routes.read().await;
206 routes.get(path).cloned()
207 }
208
209 pub async fn find_route_by_name(&self, name: &str) -> Option<WebSocketRoute> {
211 let names = self.names.read().await;
212 if let Some(path) = names.get(name) {
213 let routes = self.routes.read().await;
214 routes.get(path).cloned()
215 } else {
216 None
217 }
218 }
219
220 pub async fn remove_route(&mut self, path: &str) -> RouteResult {
222 let mut routes = self.routes.write().await;
223 let route = routes
224 .remove(path)
225 .ok_or_else(|| RouteError::NotFound(path.to_string()))?;
226 if let Some(name) = &route.name {
227 let mut names = self.names.write().await;
228 names.remove(name);
229 }
230 Ok(())
231 }
232
233 pub async fn all_routes(&self) -> Vec<WebSocketRoute> {
235 let routes = self.routes.read().await;
236 routes.values().cloned().collect()
237 }
238
239 pub async fn has_route(&self, path: &str) -> bool {
241 self.routes.read().await.contains_key(path)
242 }
243
244 pub async fn route_count(&self) -> usize {
246 self.routes.read().await.len()
247 }
248
249 pub async fn clear(&mut self) {
251 self.routes.write().await.clear();
252 self.names.write().await.clear();
253 }
254}
255
256impl Default for WebSocketRouter {
257 fn default() -> Self {
258 Self::new()
259 }
260}
261
262static GLOBAL_ROUTER: once_cell::sync::Lazy<Arc<RwLock<Option<WebSocketRouter>>>> =
265 once_cell::sync::Lazy::new(|| Arc::new(RwLock::new(None)));
266
267pub async fn register_websocket_router(router: WebSocketRouter) {
269 *GLOBAL_ROUTER.write().await = Some(router);
270}
271
272pub async fn get_websocket_router() -> Option<WebSocketRouter> {
274 GLOBAL_ROUTER.read().await.clone()
275}
276
277pub async fn clear_websocket_router() {
279 *GLOBAL_ROUTER.write().await = None;
280}
281
282pub async fn reverse_websocket_url(router: &WebSocketRouter, name: &str) -> Option<String> {
284 let names = router.names.read().await;
285 if let Some(path) = names.get(name) {
286 let routes = router.routes.read().await;
287 routes.get(path).map(|r| r.path().to_string())
288 } else {
289 router.find_pending(name).map(|r| r.path().to_string())
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use rstest::rstest;
297
298 struct TestConsumer;
299 impl WebSocketEndpointInfo for TestConsumer {
300 fn path() -> &'static str {
301 "/ws/chat/{room_id}/"
302 }
303 fn name() -> Option<&'static str> {
304 Some("chat_ws")
305 }
306 }
307
308 #[rstest]
309 fn test_substitute_no_params() {
310 assert_eq!(substitute_ws_params("/ws/notif/", &[]), "/ws/notif/");
311 }
312
313 #[rstest]
314 fn test_substitute_one_param() {
315 assert_eq!(
316 substitute_ws_params("/ws/chat/{room_id}/", &[("room_id", "42")]),
317 "/ws/chat/42/"
318 );
319 }
320
321 #[rstest]
322 fn test_consumer_builder() {
323 let router = WebSocketRouter::new().consumer(|| TestConsumer);
324 let route = router.find_pending("chat_ws");
325 assert!(route.is_some());
326 assert_eq!(route.unwrap().path(), "/ws/chat/{room_id}/");
327 }
328
329 #[rstest]
330 fn test_with_namespace_stores_value_without_rewriting_paths() {
331 let router = WebSocketRouter::new()
332 .with_namespace("auth")
333 .consumer(|| TestConsumer);
334 assert_eq!(router.namespace(), Some("auth"));
335 assert_eq!(
336 router.find_pending("chat_ws").unwrap().path(),
337 "/ws/chat/{room_id}/"
338 );
339 }
340
341 #[rstest]
342 fn test_reverse() {
343 let router = WebSocketRouter::new().consumer(|| TestConsumer);
344 assert_eq!(
345 router.reverse("chat_ws", &[("room_id", "99")]),
346 Some("/ws/chat/99/".to_string())
347 );
348 assert_eq!(router.reverse("unknown", &[]), None);
349 }
350}