use std::collections::HashMap;
use std::sync::Arc;
use rainier_http::Request;
use rainier_support::Result;
use crate::message::Message;
use crate::socket::Socket;
#[async_trait::async_trait]
pub trait WebSocketHandler: Send + Sync + 'static {
async fn on_connect(&self, socket: &Socket) -> Result<()> {
let _ = socket;
Ok(())
}
async fn on_message(&self, socket: &Socket, message: Message) -> Result<()>;
async fn on_close(&self, socket: &Socket) {
let _ = socket;
}
fn authorize(&self, request: &Request) -> bool {
let _ = request;
true
}
}
pub type Matched = (Arc<dyn WebSocketHandler>, Vec<(String, String)>);
struct Route {
pattern: String,
segments: Vec<Segment>,
handler: Arc<dyn WebSocketHandler>,
}
#[derive(Debug, Clone, PartialEq)]
enum Segment {
Literal(String),
Capture(String),
}
#[derive(Default)]
pub struct WebSocketRoutes {
routes: Vec<Route>,
}
impl WebSocketRoutes {
pub fn new() -> Self {
Self::default()
}
pub fn add(mut self, pattern: impl Into<String>, handler: impl WebSocketHandler) -> Self {
self.push(pattern, Arc::new(handler));
self
}
pub fn add_arc(
mut self,
pattern: impl Into<String>,
handler: Arc<dyn WebSocketHandler>,
) -> Self {
self.push(pattern, handler);
self
}
fn push(&mut self, pattern: impl Into<String>, handler: Arc<dyn WebSocketHandler>) {
let pattern = pattern.into();
self.routes.push(Route { segments: parse_pattern(&pattern), pattern, handler });
}
pub fn match_path(&self, path: &str) -> Option<Matched> {
self.routes.iter().find_map(|route| {
match_segments(&route.segments, path).map(|params| (Arc::clone(&route.handler), params))
})
}
pub fn patterns(&self) -> Vec<&str> {
self.routes.iter().map(|route| route.pattern.as_str()).collect()
}
pub fn len(&self) -> usize {
self.routes.len()
}
pub fn is_empty(&self) -> bool {
self.routes.is_empty()
}
}
impl std::fmt::Debug for WebSocketRoutes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebSocketRoutes").field("patterns", &self.patterns()).finish()
}
}
fn parse_pattern(pattern: &str) -> Vec<Segment> {
pattern
.trim_matches('/')
.split('/')
.map(|segment| match segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
Some(name) => Segment::Capture(name.to_string()),
None => Segment::Literal(segment.to_string()),
})
.collect()
}
fn match_segments(segments: &[Segment], path: &str) -> Option<Vec<(String, String)>> {
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
if parts.len() != segments.len() {
return None;
}
let mut params = Vec::new();
for (segment, part) in segments.iter().zip(parts) {
match segment {
Segment::Literal(literal) if literal == part => {}
Segment::Literal(_) => return None,
Segment::Capture(_) if part.is_empty() => return None,
Segment::Capture(name) => params.push((name.clone(), part.to_string())),
}
}
Some(params)
}
#[derive(Debug, Default)]
pub struct Rooms {
members: std::sync::Mutex<HashMap<String, Vec<Socket>>>,
}
impl Rooms {
pub fn new() -> Self {
Self::default()
}
pub fn join(&self, room: impl Into<String>, socket: Socket) {
let mut members = self.lock();
let room = members.entry(room.into()).or_default();
if !room.iter().any(|existing| existing.id() == socket.id()) {
room.push(socket);
}
}
pub fn leave(&self, room: &str, socket: crate::socket::SocketId) {
let mut members = self.lock();
if let Some(room_members) = members.get_mut(room) {
room_members.retain(|existing| existing.id() != socket);
}
}
pub fn leave_all(&self, socket: crate::socket::SocketId) {
let mut members = self.lock();
for room in members.values_mut() {
room.retain(|existing| existing.id() != socket);
}
members.retain(|_, room| !room.is_empty());
}
pub fn send(&self, room: &str, message: impl Into<Message>) -> usize {
self.deliver(room, message.into(), None)
}
pub fn send_except(
&self,
room: &str,
except: crate::socket::SocketId,
message: impl Into<Message>,
) -> usize {
self.deliver(room, message.into(), Some(except))
}
pub fn count(&self, room: &str) -> usize {
self.lock().get(room).map_or(0, Vec::len)
}
pub fn rooms(&self) -> Vec<String> {
self.lock().keys().cloned().collect()
}
fn deliver(
&self,
room: &str,
message: Message,
except: Option<crate::socket::SocketId>,
) -> usize {
let mut members = self.lock();
let Some(room_members) = members.get_mut(room) else { return 0 };
let mut delivered = 0;
room_members.retain(|socket| {
if Some(socket.id()) == except {
return true;
}
match socket.send(message.clone()) {
Ok(()) => {
delivered += 1;
true
}
Err(_) => false,
}
});
delivered
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<Socket>>> {
self.members.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::socket::{Outbound, SocketId};
use tokio::sync::mpsc;
struct Echo;
#[async_trait::async_trait]
impl WebSocketHandler for Echo {
async fn on_message(&self, socket: &Socket, message: Message) -> Result<()> {
socket.send(message)
}
}
fn socket(path: &str) -> (Socket, mpsc::UnboundedReceiver<Outbound>) {
let (tx, rx) = mpsc::unbounded_channel();
(Socket::new(SocketId::next(), path, Vec::new(), tx), rx)
}
#[test]
fn a_pattern_captures_its_segments() {
let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
assert_eq!(params, vec![("room".to_string(), "lobby".to_string())]);
}
#[test]
fn a_pattern_does_not_swallow_extra_segments() {
let routes = WebSocketRoutes::new().add("/ws/rooms/{room}", Echo);
assert!(routes.match_path("/ws/rooms/lobby/messages").is_none());
assert!(routes.match_path("/ws/rooms").is_none());
}
#[test]
fn an_undeclared_path_matches_nothing() {
let routes = WebSocketRoutes::new().add("/ws/chat", Echo);
assert!(routes.match_path("/ws/other").is_none());
}
#[test]
fn the_first_matching_pattern_wins() {
let routes =
WebSocketRoutes::new().add("/ws/rooms/lobby", Echo).add("/ws/rooms/{room}", Echo);
let (_, params) = routes.match_path("/ws/rooms/lobby").expect("matches");
assert!(params.is_empty(), "the literal route should win");
}
#[tokio::test]
async fn a_room_delivers_to_its_members() {
let rooms = Rooms::new();
let (first, mut first_rx) = socket("/ws");
let (second, mut second_rx) = socket("/ws");
rooms.join("lobby", first.clone());
rooms.join("lobby", second);
assert_eq!(rooms.send("lobby", "hello"), 2);
assert!(first_rx.try_recv().is_ok());
assert!(second_rx.try_recv().is_ok());
}
#[tokio::test]
async fn send_except_skips_the_sender() {
let rooms = Rooms::new();
let (first, mut first_rx) = socket("/ws");
let (second, mut second_rx) = socket("/ws");
let sender = first.id();
rooms.join("lobby", first);
rooms.join("lobby", second);
assert_eq!(rooms.send_except("lobby", sender, "hello"), 1);
assert!(first_rx.try_recv().is_err(), "the sender should not get their own message");
assert!(second_rx.try_recv().is_ok());
}
#[tokio::test]
async fn joining_twice_delivers_once() {
let rooms = Rooms::new();
let (socket, _rx) = socket("/ws");
rooms.join("lobby", socket.clone());
rooms.join("lobby", socket);
assert_eq!(rooms.count("lobby"), 1);
}
#[tokio::test]
async fn a_socket_that_went_away_is_dropped_on_the_next_send() {
let rooms = Rooms::new();
let (alive, _alive_rx) = socket("/ws");
let (gone, gone_rx) = socket("/ws");
rooms.join("lobby", alive);
rooms.join("lobby", gone);
drop(gone_rx);
assert_eq!(rooms.send("lobby", "hello"), 1, "only the live one received it");
assert_eq!(rooms.count("lobby"), 1, "and the dead handle is gone");
}
#[tokio::test]
async fn leaving_every_room_is_one_call() {
let rooms = Rooms::new();
let (socket, _rx) = socket("/ws");
let id = socket.id();
rooms.join("a", socket.clone());
rooms.join("b", socket);
rooms.leave_all(id);
assert_eq!(rooms.count("a"), 0);
assert!(rooms.rooms().is_empty(), "empty rooms are forgotten");
}
}