1use crate::error::WsError;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8#[derive(Debug, Clone)]
9pub struct WebSocketMessage {
10 pub msg_type: MessageType,
11 pub payload: Vec<u8>,
12 pub sender_id: Option<i64>,
13 pub room_id: Option<String>,
14 pub timestamp: i64,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
18pub enum MessageType {
19 #[default]
20 Text,
21 Binary,
22 Ping,
23 Pong,
24 Join,
25 Leave,
26 Subscribe,
27 Unsubscribe,
28 Notification,
29 System,
30}
31
32#[derive(Debug, Clone)]
33pub struct WebSocketConnection {
34 pub id: String,
35 pub user_id: Option<i64>,
36 pub remote_addr: Option<String>,
37 pub is_authenticated: bool,
38 pub subscriptions: Vec<String>,
39}
40
41impl WebSocketConnection {
42 pub fn new(id: impl Into<String>) -> Self {
43 Self {
44 id: id.into(),
45 user_id: None,
46 remote_addr: None,
47 is_authenticated: false,
48 subscriptions: Vec::new(),
49 }
50 }
51
52 pub fn with_user(mut self, user_id: i64) -> Self {
53 self.user_id = Some(user_id);
54 self.is_authenticated = true;
55 self
56 }
57
58 pub fn with_address(mut self, addr: impl Into<String>) -> Self {
59 self.remote_addr = Some(addr.into());
60 self
61 }
62
63 pub fn subscribe(&mut self, room: impl Into<String>) {
64 let room = room.into();
65 if !self.subscriptions.contains(&room) {
66 self.subscriptions.push(room);
67 }
68 }
69
70 pub fn unsubscribe(&mut self, room: &str) {
71 self.subscriptions.retain(|r| r != room);
72 }
73}
74
75#[async_trait]
76pub trait WebSocketHandler: Send + Sync {
77 async fn on_message(
78 &self,
79 conn: &WebSocketConnection,
80 msg: WebSocketMessage,
81 ) -> Result<Option<WebSocketMessage>, WsError>;
82
83 async fn on_connect(&self, conn: &WebSocketConnection) -> Result<(), WsError>;
84
85 async fn on_disconnect(&self, conn: &WebSocketConnection);
86
87 fn authenticate(&self, token: &str) -> Result<UserId, WsError>;
88}
89
90pub type UserId = i64;
91
92pub struct WsContext {
93 pub connection_id: String,
94 pub user_id: Option<i64>,
95 pub metadata: std::collections::HashMap<String, String>,
96}
97
98impl WsContext {
99 pub fn new(connection_id: impl Into<String>) -> Self {
100 Self {
101 connection_id: connection_id.into(),
102 user_id: None,
103 metadata: std::collections::HashMap::new(),
104 }
105 }
106
107 pub fn with_user(mut self, user_id: i64) -> Self {
108 self.user_id = Some(user_id);
109 self
110 }
111
112 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
113 self.metadata.insert(key.into(), value.into());
114 self
115 }
116}
117
118pub struct WsMessageBuilder {
119 msg_type: MessageType,
120 payload: Vec<u8>,
121 sender_id: Option<i64>,
122 room_id: Option<String>,
123}
124
125impl WsMessageBuilder {
126 pub fn new() -> Self {
127 Self {
128 msg_type: MessageType::Text,
129 payload: Vec::new(),
130 sender_id: None,
131 room_id: None,
132 }
133 }
134
135 pub fn text(mut self, text: impl Into<String>) -> Self {
136 self.msg_type = MessageType::Text;
137 self.payload = text.into().into_bytes();
138 self
139 }
140
141 pub fn binary(mut self, data: Vec<u8>) -> Self {
142 self.msg_type = MessageType::Binary;
143 self.payload = data;
144 self
145 }
146
147 pub fn json<T: serde::Serialize>(mut self, data: &T) -> Result<Self, WsError> {
148 self.msg_type = MessageType::Text;
149 self.payload = serde_json::to_vec(data)?;
150 Ok(self)
151 }
152
153 pub fn with_sender(mut self, user_id: i64) -> Self {
154 self.sender_id = Some(user_id);
155 self
156 }
157
158 pub fn with_room(mut self, room: impl Into<String>) -> Self {
159 self.room_id = Some(room.into());
160 self
161 }
162
163 pub fn notification(mut self) -> Self {
164 self.msg_type = MessageType::Notification;
165 self
166 }
167
168 pub fn system(mut self) -> Self {
169 self.msg_type = MessageType::System;
170 self
171 }
172
173 pub fn build(self) -> WebSocketMessage {
174 WebSocketMessage {
175 msg_type: self.msg_type,
176 payload: self.payload,
177 sender_id: self.sender_id,
178 room_id: self.room_id,
179 timestamp: current_timestamp(),
180 }
181 }
182}
183
184impl Default for WsMessageBuilder {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190fn current_timestamp() -> i64 {
191 use std::time::{SystemTime, UNIX_EPOCH};
192 SystemTime::now()
193 .duration_since(UNIX_EPOCH)
194 .unwrap_or_default()
195 .as_millis() as i64
196}
197
198pub struct DefaultWebSocketHandler {
205 connections: Arc<RwLock<HashMap<String, WebSocketConnection>>>,
206 message_log: Arc<RwLock<Vec<WebSocketMessage>>>,
207}
208
209impl DefaultWebSocketHandler {
210 pub fn new() -> Self {
211 Self {
212 connections: Arc::new(RwLock::new(HashMap::new())),
213 message_log: Arc::new(RwLock::new(Vec::new())),
214 }
215 }
216
217 pub async fn connection_count(&self) -> usize {
218 self.connections.read().await.len()
219 }
220
221 pub async fn is_connected(&self, connection_id: &str) -> bool {
222 self.connections.read().await.contains_key(connection_id)
223 }
224
225 pub async fn message_count(&self) -> usize {
226 self.message_log.read().await.len()
227 }
228
229 pub async fn messages(&self) -> Vec<WebSocketMessage> {
230 self.message_log.read().await.clone()
231 }
232
233 pub async fn get_connection(&self, connection_id: &str) -> Option<WebSocketConnection> {
234 self.connections.read().await.get(connection_id).cloned()
235 }
236}
237
238impl Default for DefaultWebSocketHandler {
239 fn default() -> Self {
240 Self::new()
241 }
242}
243
244#[async_trait]
245impl WebSocketHandler for DefaultWebSocketHandler {
246 async fn on_message(
247 &self,
248 conn: &WebSocketConnection,
249 msg: WebSocketMessage,
250 ) -> Result<Option<WebSocketMessage>, WsError> {
251 self.message_log.write().await.push(msg.clone());
252
253 match msg.msg_type {
254 MessageType::Text => {
255 let response = WebSocketMessage {
256 msg_type: MessageType::Text,
257 payload: msg.payload.clone(),
258 sender_id: conn.user_id,
259 room_id: None,
260 timestamp: current_timestamp(),
261 };
262 Ok(Some(response))
263 }
264 MessageType::Ping => {
265 let response = WebSocketMessage {
266 msg_type: MessageType::Pong,
267 payload: msg.payload.clone(),
268 sender_id: None,
269 room_id: None,
270 timestamp: current_timestamp(),
271 };
272 Ok(Some(response))
273 }
274 MessageType::Subscribe => {
275 let room = String::from_utf8_lossy(&msg.payload).to_string();
276 let ack = format!("subscribed:{}", room);
277 let response = WebSocketMessage {
278 msg_type: MessageType::System,
279 payload: ack.into_bytes(),
280 sender_id: None,
281 room_id: Some(room),
282 timestamp: current_timestamp(),
283 };
284 Ok(Some(response))
285 }
286 MessageType::Unsubscribe => {
287 let room = String::from_utf8_lossy(&msg.payload).to_string();
288 let ack = format!("unsubscribed:{}", room);
289 let response = WebSocketMessage {
290 msg_type: MessageType::System,
291 payload: ack.into_bytes(),
292 sender_id: None,
293 room_id: Some(room),
294 timestamp: current_timestamp(),
295 };
296 Ok(Some(response))
297 }
298 MessageType::Join => {
299 let room = String::from_utf8_lossy(&msg.payload).to_string();
300 let ack = format!("joined:{}", room);
301 let response = WebSocketMessage {
302 msg_type: MessageType::System,
303 payload: ack.into_bytes(),
304 sender_id: conn.user_id,
305 room_id: Some(room),
306 timestamp: current_timestamp(),
307 };
308 Ok(Some(response))
309 }
310 MessageType::Leave => {
311 let room = String::from_utf8_lossy(&msg.payload).to_string();
312 let ack = format!("left:{}", room);
313 let response = WebSocketMessage {
314 msg_type: MessageType::System,
315 payload: ack.into_bytes(),
316 sender_id: conn.user_id,
317 room_id: Some(room),
318 timestamp: current_timestamp(),
319 };
320 Ok(Some(response))
321 }
322 _ => Ok(None),
323 }
324 }
325
326 async fn on_connect(&self, conn: &WebSocketConnection) -> Result<(), WsError> {
327 self.connections
328 .write()
329 .await
330 .insert(conn.id.clone(), conn.clone());
331 Ok(())
332 }
333
334 async fn on_disconnect(&self, conn: &WebSocketConnection) {
335 self.connections.write().await.remove(&conn.id);
336 }
337
338 fn authenticate(&self, token: &str) -> Result<UserId, WsError> {
339 if let Some(id_str) = token.strip_prefix("user_id:") {
340 id_str.parse::<i64>().map_err(|_| {
341 WsError::Authentication(format!("invalid user_id in token: {}", token))
342 })
343 } else {
344 token
345 .parse::<i64>()
346 .map_err(|_| WsError::Authentication(format!("invalid token: {}", token)))
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 fn make_conn(id: &str, user_id: Option<i64>) -> WebSocketConnection {
356 let mut conn = WebSocketConnection::new(id);
357 if let Some(uid) = user_id {
358 conn = conn.with_user(uid);
359 }
360 conn
361 }
362
363 fn make_text(payload: &[u8]) -> WebSocketMessage {
364 WebSocketMessage {
365 msg_type: MessageType::Text,
366 payload: payload.to_vec(),
367 sender_id: None,
368 room_id: None,
369 timestamp: 1000,
370 }
371 }
372
373 #[tokio::test]
374 async fn test_on_connect_tracks_connection() {
375 let handler = DefaultWebSocketHandler::new();
376 assert_eq!(handler.connection_count().await, 0);
377
378 let conn = make_conn("c1", Some(123));
379 handler.on_connect(&conn).await.unwrap();
380
381 assert_eq!(handler.connection_count().await, 1);
382 assert!(handler.is_connected("c1").await);
383 }
384
385 #[tokio::test]
386 async fn test_on_disconnect_removes_connection() {
387 let handler = DefaultWebSocketHandler::new();
388 let conn = make_conn("c1", Some(123));
389 handler.on_connect(&conn).await.unwrap();
390 assert!(handler.is_connected("c1").await);
391
392 handler.on_disconnect(&conn).await;
393 assert!(!handler.is_connected("c1").await);
394 assert_eq!(handler.connection_count().await, 0);
395 }
396
397 #[tokio::test]
398 async fn test_on_message_text_echoes_back() {
399 let handler = DefaultWebSocketHandler::new();
400 let conn = make_conn("c1", Some(42));
401 let msg = make_text(b"hello");
402
403 let response = handler.on_message(&conn, msg).await.unwrap();
404 assert!(response.is_some());
405
406 let resp = response.unwrap();
407 assert_eq!(resp.msg_type, MessageType::Text);
408 assert_eq!(resp.payload, b"hello");
409 assert_eq!(resp.sender_id, Some(42));
410 }
411
412 #[tokio::test]
413 async fn test_on_message_ping_responds_pong() {
414 let handler = DefaultWebSocketHandler::new();
415 let conn = make_conn("c1", None);
416 let msg = WebSocketMessage {
417 msg_type: MessageType::Ping,
418 payload: b"ping".to_vec(),
419 sender_id: None,
420 room_id: None,
421 timestamp: 1,
422 };
423
424 let response = handler.on_message(&conn, msg).await.unwrap();
425 let resp = response.unwrap();
426 assert_eq!(resp.msg_type, MessageType::Pong);
427 assert_eq!(resp.payload, b"ping");
428 }
429
430 #[tokio::test]
431 async fn test_on_message_subscribe_returns_system_ack() {
432 let handler = DefaultWebSocketHandler::new();
433 let conn = make_conn("c1", None);
434 let msg = WebSocketMessage {
435 msg_type: MessageType::Subscribe,
436 payload: b"room1".to_vec(),
437 sender_id: None,
438 room_id: None,
439 timestamp: 1,
440 };
441
442 let response = handler.on_message(&conn, msg).await.unwrap();
443 let resp = response.unwrap();
444 assert_eq!(resp.msg_type, MessageType::System);
445 assert_eq!(resp.payload, b"subscribed:room1");
446 assert_eq!(resp.room_id, Some("room1".to_string()));
447 }
448
449 #[tokio::test]
450 async fn test_on_message_unsubscribe_returns_system_ack() {
451 let handler = DefaultWebSocketHandler::new();
452 let conn = make_conn("c1", None);
453 let msg = WebSocketMessage {
454 msg_type: MessageType::Unsubscribe,
455 payload: b"room1".to_vec(),
456 sender_id: None,
457 room_id: None,
458 timestamp: 1,
459 };
460
461 let response = handler.on_message(&conn, msg).await.unwrap();
462 let resp = response.unwrap();
463 assert_eq!(resp.msg_type, MessageType::System);
464 assert_eq!(resp.payload, b"unsubscribed:room1");
465 }
466
467 #[tokio::test]
468 async fn test_on_message_join_returns_system_ack() {
469 let handler = DefaultWebSocketHandler::new();
470 let conn = make_conn("c1", Some(7));
471 let msg = WebSocketMessage {
472 msg_type: MessageType::Join,
473 payload: b"lobby".to_vec(),
474 sender_id: None,
475 room_id: None,
476 timestamp: 1,
477 };
478
479 let response = handler.on_message(&conn, msg).await.unwrap();
480 let resp = response.unwrap();
481 assert_eq!(resp.msg_type, MessageType::System);
482 assert_eq!(resp.payload, b"joined:lobby");
483 assert_eq!(resp.sender_id, Some(7));
484 assert_eq!(resp.room_id, Some("lobby".to_string()));
485 }
486
487 #[tokio::test]
488 async fn test_on_message_leave_returns_system_ack() {
489 let handler = DefaultWebSocketHandler::new();
490 let conn = make_conn("c1", Some(7));
491 let msg = WebSocketMessage {
492 msg_type: MessageType::Leave,
493 payload: b"lobby".to_vec(),
494 sender_id: None,
495 room_id: None,
496 timestamp: 1,
497 };
498
499 let response = handler.on_message(&conn, msg).await.unwrap();
500 let resp = response.unwrap();
501 assert_eq!(resp.payload, b"left:lobby");
502 }
503
504 #[tokio::test]
505 async fn test_on_message_binary_returns_none() {
506 let handler = DefaultWebSocketHandler::new();
507 let conn = make_conn("c1", None);
508 let msg = WebSocketMessage {
509 msg_type: MessageType::Binary,
510 payload: vec![1, 2, 3],
511 sender_id: None,
512 room_id: None,
513 timestamp: 1,
514 };
515
516 let response = handler.on_message(&conn, msg).await.unwrap();
517 assert!(response.is_none());
518 }
519
520 #[tokio::test]
521 async fn test_message_log_records_all_messages() {
522 let handler = DefaultWebSocketHandler::new();
523 let conn = make_conn("c1", None);
524
525 handler.on_message(&conn, make_text(b"m1")).await.unwrap();
526 handler
527 .on_message(
528 &conn,
529 WebSocketMessage {
530 msg_type: MessageType::Binary,
531 payload: vec![1],
532 sender_id: None,
533 room_id: None,
534 timestamp: 2,
535 },
536 )
537 .await
538 .unwrap();
539 handler.on_message(&conn, make_text(b"m3")).await.unwrap();
540
541 assert_eq!(handler.message_count().await, 3);
542 let msgs = handler.messages().await;
543 assert_eq!(msgs[0].payload, b"m1");
544 assert_eq!(msgs[1].msg_type, MessageType::Binary);
545 assert_eq!(msgs[2].payload, b"m3");
546 }
547
548 #[tokio::test]
549 async fn test_authenticate_valid_numeric_token() {
550 let handler = DefaultWebSocketHandler::new();
551 let user_id = handler.authenticate("12345").unwrap();
552 assert_eq!(user_id, 12345);
553 }
554
555 #[tokio::test]
556 async fn test_authenticate_valid_prefixed_token() {
557 let handler = DefaultWebSocketHandler::new();
558 let user_id = handler.authenticate("user_id:67890").unwrap();
559 assert_eq!(user_id, 67890);
560 }
561
562 #[test]
563 fn test_authenticate_invalid_token_returns_error() {
564 let handler = DefaultWebSocketHandler::new();
565 let result = handler.authenticate("not-a-number");
566 assert!(result.is_err());
567 assert!(matches!(result.unwrap_err(), WsError::Authentication(_)));
568 }
569
570 #[test]
571 fn test_authenticate_invalid_prefixed_token_returns_error() {
572 let handler = DefaultWebSocketHandler::new();
573 let result = handler.authenticate("user_id:abc");
574 assert!(result.is_err());
575 assert!(matches!(result.unwrap_err(), WsError::Authentication(_)));
576 }
577
578 #[tokio::test]
579 async fn test_get_connection_returns_stored_connection() {
580 let handler = DefaultWebSocketHandler::new();
581 let conn = make_conn("c1", Some(99));
582 handler.on_connect(&conn).await.unwrap();
583
584 let retrieved = handler.get_connection("c1").await.unwrap();
585 assert_eq!(retrieved.id, "c1");
586 assert_eq!(retrieved.user_id, Some(99));
587 assert!(retrieved.is_authenticated);
588 }
589
590 #[tokio::test]
591 async fn test_get_connection_not_found() {
592 let handler = DefaultWebSocketHandler::new();
593 assert!(handler.get_connection("missing").await.is_none());
594 }
595
596 #[tokio::test]
597 async fn test_multiple_connections_tracked_independently() {
598 let handler = DefaultWebSocketHandler::new();
599 let conn1 = make_conn("c1", Some(1));
600 let conn2 = make_conn("c2", Some(2));
601
602 handler.on_connect(&conn1).await.unwrap();
603 handler.on_connect(&conn2).await.unwrap();
604
605 assert_eq!(handler.connection_count().await, 2);
606
607 handler.on_disconnect(&conn1).await;
608 assert_eq!(handler.connection_count().await, 1);
609 assert!(!handler.is_connected("c1").await);
610 assert!(handler.is_connected("c2").await);
611 }
612
613 #[tokio::test]
614 async fn test_default_impl_creates_empty_handler() {
615 let handler = DefaultWebSocketHandler::default();
616 assert_eq!(handler.connection_count().await, 0);
617 assert_eq!(handler.message_count().await, 0);
618 }
619}