1use std::collections::HashMap;
4use std::sync::Arc;
5
6use rainier_http::Request;
7use rainier_support::Result;
8
9use crate::message::Message;
10use crate::socket::Socket;
11
12#[async_trait::async_trait]
41pub trait WebSocketHandler: Send + Sync + 'static {
42 async fn on_connect(&self, socket: &Socket) -> Result<()> {
47 let _ = socket;
48 Ok(())
49 }
50
51 async fn on_message(&self, socket: &Socket, message: Message) -> Result<()>;
53
54 async fn on_close(&self, socket: &Socket) {
59 let _ = socket;
60 }
61
62 fn authorize(&self, request: &Request) -> bool {
73 let _ = request;
74 true
75 }
76}
77
78pub type Matched = (Arc<dyn WebSocketHandler>, Vec<(String, String)>);
80
81struct Route {
83 pattern: String,
84 segments: Vec<Segment>,
85 handler: Arc<dyn WebSocketHandler>,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89enum Segment {
90 Literal(String),
91 Capture(String),
92}
93
94#[derive(Default)]
108pub struct WebSocketRoutes {
109 routes: Vec<Route>,
110}
111
112impl WebSocketRoutes {
113 pub fn new() -> Self {
115 Self::default()
116 }
117
118 pub fn add(mut self, pattern: impl Into<String>, handler: impl WebSocketHandler) -> Self {
124 self.push(pattern, Arc::new(handler));
125 self
126 }
127
128 pub fn add_arc(
130 mut self,
131 pattern: impl Into<String>,
132 handler: Arc<dyn WebSocketHandler>,
133 ) -> Self {
134 self.push(pattern, handler);
135 self
136 }
137
138 fn push(&mut self, pattern: impl Into<String>, handler: Arc<dyn WebSocketHandler>) {
139 let pattern = pattern.into();
140 self.routes.push(Route { segments: parse_pattern(&pattern), pattern, handler });
141 }
142
143 pub fn match_path(&self, path: &str) -> Option<Matched> {
145 self.routes.iter().find_map(|route| {
146 match_segments(&route.segments, path).map(|params| (Arc::clone(&route.handler), params))
147 })
148 }
149
150 pub fn patterns(&self) -> Vec<&str> {
152 self.routes.iter().map(|route| route.pattern.as_str()).collect()
153 }
154
155 pub fn len(&self) -> usize {
157 self.routes.len()
158 }
159
160 pub fn is_empty(&self) -> bool {
162 self.routes.is_empty()
163 }
164}
165
166impl std::fmt::Debug for WebSocketRoutes {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 f.debug_struct("WebSocketRoutes").field("patterns", &self.patterns()).finish()
169 }
170}
171
172fn parse_pattern(pattern: &str) -> Vec<Segment> {
173 pattern
174 .trim_matches('/')
175 .split('/')
176 .map(|segment| match segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
177 Some(name) => Segment::Capture(name.to_string()),
178 None => Segment::Literal(segment.to_string()),
179 })
180 .collect()
181}
182
183fn match_segments(segments: &[Segment], path: &str) -> Option<Vec<(String, String)>> {
189 let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
190 if parts.len() != segments.len() {
191 return None;
192 }
193
194 let mut params = Vec::new();
195 for (segment, part) in segments.iter().zip(parts) {
196 match segment {
197 Segment::Literal(literal) if literal == part => {}
198 Segment::Literal(_) => return None,
199 Segment::Capture(_) if part.is_empty() => return None,
200 Segment::Capture(name) => params.push((name.clone(), part.to_string())),
201 }
202 }
203 Some(params)
204}
205
206#[derive(Debug, Default)]
222pub struct Rooms {
223 members: std::sync::Mutex<HashMap<String, Vec<Socket>>>,
224}
225
226impl Rooms {
227 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn join(&self, room: impl Into<String>, socket: Socket) {
234 let mut members = self.lock();
235 let room = members.entry(room.into()).or_default();
236
237 if !room.iter().any(|existing| existing.id() == socket.id()) {
240 room.push(socket);
241 }
242 }
243
244 pub fn leave(&self, room: &str, socket: crate::socket::SocketId) {
246 let mut members = self.lock();
247 if let Some(room_members) = members.get_mut(room) {
248 room_members.retain(|existing| existing.id() != socket);
249 }
250 }
251
252 pub fn leave_all(&self, socket: crate::socket::SocketId) {
257 let mut members = self.lock();
258 for room in members.values_mut() {
259 room.retain(|existing| existing.id() != socket);
260 }
261 members.retain(|_, room| !room.is_empty());
262 }
263
264 pub fn send(&self, room: &str, message: impl Into<Message>) -> usize {
266 self.deliver(room, message.into(), None)
267 }
268
269 pub fn send_except(
272 &self,
273 room: &str,
274 except: crate::socket::SocketId,
275 message: impl Into<Message>,
276 ) -> usize {
277 self.deliver(room, message.into(), Some(except))
278 }
279
280 pub fn count(&self, room: &str) -> usize {
282 self.lock().get(room).map_or(0, Vec::len)
283 }
284
285 pub fn rooms(&self) -> Vec<String> {
287 self.lock().keys().cloned().collect()
288 }
289
290 fn deliver(
291 &self,
292 room: &str,
293 message: Message,
294 except: Option<crate::socket::SocketId>,
295 ) -> usize {
296 let mut members = self.lock();
297 let Some(room_members) = members.get_mut(room) else { return 0 };
298
299 let mut delivered = 0;
300 room_members.retain(|socket| {
301 if Some(socket.id()) == except {
302 return true;
303 }
304 match socket.send(message.clone()) {
305 Ok(()) => {
306 delivered += 1;
307 true
308 }
309 Err(_) => false,
311 }
312 });
313 delivered
314 }
315
316 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<Socket>>> {
317 self.members.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::socket::{Outbound, SocketId};
325 use tokio::sync::mpsc;
326
327 struct Echo;
328
329 #[async_trait::async_trait]
330 impl WebSocketHandler for Echo {
331 async fn on_message(&self, socket: &Socket, message: Message) -> Result<()> {
332 socket.send(message)
333 }
334 }
335
336 fn socket(path: &str) -> (Socket, mpsc::UnboundedReceiver<Outbound>) {
337 let (tx, rx) = mpsc::unbounded_channel();
338 (Socket::new(SocketId::next(), path, Vec::new(), tx), rx)
339 }
340
341 #[test]
342 fn a_pattern_captures_its_segments() {
343 let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
344
345 let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
346 assert_eq!(params, vec![("room".to_string(), "lobby".to_string())]);
347 }
348
349 #[test]
350 fn a_pattern_does_not_swallow_extra_segments() {
351 let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
352
353 assert!(routes.match_path("/ws/rooms/lobby/messages").is_none());
354 assert!(routes.match_path("/ws/rooms").is_none());
355 }
356
357 #[test]
358 fn an_undeclared_path_matches_nothing() {
359 let routes = WebSocketRoutes::new().add("/ws/chat", Echo);
360 assert!(routes.match_path("/ws/other").is_none());
361 }
362
363 #[test]
364 fn the_first_matching_pattern_wins() {
365 let routes =
366 WebSocketRoutes::new().add("/ws/rooms/lobby", Echo).add("/ws/rooms/{room}", Echo);
367
368 let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
369 assert!(params.is_empty(), "the literal route should win");
370 }
371
372 #[tokio::test]
373 async fn a_room_delivers_to_its_members() {
374 let rooms = Rooms::new();
375 let (first, mut first_rx) = socket("/ws");
376 let (second, mut second_rx) = socket("/ws");
377
378 rooms.join("lobby", first.clone());
379 rooms.join("lobby", second);
380
381 assert_eq!(rooms.send("lobby", "hello"), 2);
382 assert!(first_rx.try_recv().is_ok());
383 assert!(second_rx.try_recv().is_ok());
384 }
385
386 #[tokio::test]
387 async fn send_except_skips_the_sender() {
388 let rooms = Rooms::new();
389 let (first, mut first_rx) = socket("/ws");
390 let (second, mut second_rx) = socket("/ws");
391 let sender = first.id();
392
393 rooms.join("lobby", first);
394 rooms.join("lobby", second);
395
396 assert_eq!(rooms.send_except("lobby", sender, "hello"), 1);
397 assert!(first_rx.try_recv().is_err(), "the sender should not get their own message");
398 assert!(second_rx.try_recv().is_ok());
399 }
400
401 #[tokio::test]
402 async fn joining_twice_delivers_once() {
403 let rooms = Rooms::new();
404 let (socket, _rx) = socket("/ws");
405
406 rooms.join("lobby", socket.clone());
407 rooms.join("lobby", socket);
408
409 assert_eq!(rooms.count("lobby"), 1);
410 }
411
412 #[tokio::test]
413 async fn a_socket_that_went_away_is_dropped_on_the_next_send() {
414 let rooms = Rooms::new();
415 let (alive, _alive_rx) = socket("/ws");
416 let (gone, gone_rx) = socket("/ws");
417
418 rooms.join("lobby", alive);
419 rooms.join("lobby", gone);
420 drop(gone_rx);
421
422 assert_eq!(rooms.send("lobby", "hello"), 1, "only the live one received it");
423 assert_eq!(rooms.count("lobby"), 1, "and the dead handle is gone");
424 }
425
426 #[tokio::test]
427 async fn leaving_every_room_is_one_call() {
428 let rooms = Rooms::new();
429 let (socket, _rx) = socket("/ws");
430 let id = socket.id();
431
432 rooms.join("a", socket.clone());
433 rooms.join("b", socket);
434 rooms.leave_all(id);
435
436 assert_eq!(rooms.count("a"), 0);
437 assert!(rooms.rooms().is_empty(), "empty rooms are forgotten");
438 }
439}