use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio_tungstenite::tungstenite::protocol::Message as WsMessage;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BroadcastMessage {
pub event: String,
pub channel: String,
pub data: Value,
}
impl BroadcastMessage {
pub fn new(channel: impl Into<String>, event: impl Into<String>, data: impl Serialize) -> Self {
Self {
channel: channel.into(),
event: event.into(),
data: serde_json::to_value(data).unwrap_or(Value::Null),
}
}
pub fn with_data(channel: impl Into<String>, event: impl Into<String>, data: Value) -> Self {
Self {
channel: channel.into(),
event: event.into(),
data,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_ws_message(&self) -> Result<WsMessage, serde_json::Error> {
let json = serde_json::to_string(self)?;
Ok(WsMessage::Text(json.into()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Subscribe {
channel: String,
#[serde(default)]
auth: Option<String>,
#[serde(default)]
channel_data: Option<Value>,
},
Unsubscribe { channel: String },
Whisper {
channel: String,
event: String,
data: Value,
},
Ping,
}
impl ClientMessage {
pub fn from_ws_text(text: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(text)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
Connected { socket_id: String },
Subscribed { channel: String },
SubscriptionError { channel: String, error: String },
Unsubscribed { channel: String },
Event(BroadcastMessage),
MemberAdded {
channel: String,
user_id: String,
user_info: Value,
},
MemberRemoved { channel: String, user_id: String },
Pong,
Error { message: String },
}
impl ServerMessage {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn to_ws_message(&self) -> Result<WsMessage, serde_json::Error> {
let json = serde_json::to_string(self)?;
Ok(WsMessage::Text(json.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_broadcast_message() {
let msg = BroadcastMessage::new("orders.1", "OrderUpdated", serde_json::json!({"id": 1}));
assert_eq!(msg.channel, "orders.1");
assert_eq!(msg.event, "OrderUpdated");
}
#[test]
fn test_client_message_serialize() {
let msg = ClientMessage::Subscribe {
channel: "private-orders.1".into(),
auth: Some("auth_token".into()),
channel_data: None,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("subscribe"));
assert!(json.contains("private-orders.1"));
}
#[test]
fn test_subscribe_with_channel_data() {
let json = r#"{"type":"subscribe","channel":"presence-nearby","auth":"ok","channel_data":{"user_id":"42","user_info":{"name":"Alice"}}}"#;
let msg: ClientMessage = serde_json::from_str(json).unwrap();
match msg {
ClientMessage::Subscribe {
channel,
auth,
channel_data,
} => {
assert_eq!(channel, "presence-nearby");
assert_eq!(auth.unwrap(), "ok");
let data = channel_data.unwrap();
assert_eq!(data["user_id"], "42");
assert_eq!(data["user_info"]["name"], "Alice");
}
_ => panic!("Expected Subscribe"),
}
}
#[test]
fn test_subscribe_without_channel_data() {
let json = r#"{"type":"subscribe","channel":"presence-nearby","auth":"ok"}"#;
let msg: ClientMessage = serde_json::from_str(json).unwrap();
match msg {
ClientMessage::Subscribe { channel_data, .. } => {
assert!(channel_data.is_none());
}
_ => panic!("Expected Subscribe"),
}
}
#[test]
fn test_server_message_serialize() {
let msg = ServerMessage::Connected {
socket_id: "abc123".into(),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("connected"));
assert!(json.contains("abc123"));
}
}