Skip to main content

ferro_broadcast/
message.rs

1//! Broadcast message types.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use tokio_tungstenite::tungstenite::protocol::Message as WsMessage;
6
7/// A message that can be broadcast to channels.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct BroadcastMessage {
10    /// The event name.
11    pub event: String,
12    /// The channel name.
13    pub channel: String,
14    /// The message data.
15    pub data: Value,
16}
17
18impl BroadcastMessage {
19    /// Create a new broadcast message.
20    pub fn new(channel: impl Into<String>, event: impl Into<String>, data: impl Serialize) -> Self {
21        Self {
22            channel: channel.into(),
23            event: event.into(),
24            data: serde_json::to_value(data).unwrap_or(Value::Null),
25        }
26    }
27
28    /// Create with raw JSON data.
29    pub fn with_data(channel: impl Into<String>, event: impl Into<String>, data: Value) -> Self {
30        Self {
31            channel: channel.into(),
32            event: event.into(),
33            data,
34        }
35    }
36
37    /// Serialize to JSON string.
38    pub fn to_json(&self) -> Result<String, serde_json::Error> {
39        serde_json::to_string(self)
40    }
41
42    /// Convert to a WebSocket text message.
43    pub fn to_ws_message(&self) -> Result<WsMessage, serde_json::Error> {
44        let json = serde_json::to_string(self)?;
45        Ok(WsMessage::Text(json.into()))
46    }
47}
48
49/// A client-to-server message.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(tag = "type", rename_all = "snake_case")]
52pub enum ClientMessage {
53    /// Subscribe to a channel.
54    Subscribe {
55        channel: String,
56        #[serde(default)]
57        auth: Option<String>,
58        /// Optional member data for presence channels (e.g. `{"user_id": "42", "user_info": {"name": "Alice"}}`).
59        #[serde(default)]
60        channel_data: Option<Value>,
61    },
62    /// Unsubscribe from a channel.
63    Unsubscribe { channel: String },
64    /// Send a message to a channel (client events).
65    Whisper {
66        channel: String,
67        event: String,
68        data: Value,
69    },
70    /// Ping to keep connection alive.
71    Ping,
72}
73
74impl ClientMessage {
75    /// Parse a client message from WebSocket text payload.
76    pub fn from_ws_text(text: &str) -> Result<Self, serde_json::Error> {
77        serde_json::from_str(text)
78    }
79}
80
81/// A server-to-client message.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(tag = "type", rename_all = "snake_case")]
84pub enum ServerMessage {
85    /// Connection established.
86    Connected { socket_id: String },
87    /// Subscription successful.
88    Subscribed { channel: String },
89    /// Subscription failed.
90    SubscriptionError { channel: String, error: String },
91    /// Unsubscribed from channel.
92    Unsubscribed { channel: String },
93    /// Broadcast event.
94    Event(BroadcastMessage),
95    /// Member joined (presence channels).
96    MemberAdded {
97        channel: String,
98        user_id: String,
99        user_info: Value,
100    },
101    /// Member left (presence channels).
102    MemberRemoved { channel: String, user_id: String },
103    /// Pong response.
104    Pong,
105    /// Error message.
106    Error { message: String },
107}
108
109impl ServerMessage {
110    /// Serialize to JSON string.
111    pub fn to_json(&self) -> Result<String, serde_json::Error> {
112        serde_json::to_string(self)
113    }
114
115    /// Convert to a WebSocket text message.
116    pub fn to_ws_message(&self) -> Result<WsMessage, serde_json::Error> {
117        let json = serde_json::to_string(self)?;
118        Ok(WsMessage::Text(json.into()))
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_broadcast_message() {
128        let msg = BroadcastMessage::new("orders.1", "OrderUpdated", serde_json::json!({"id": 1}));
129        assert_eq!(msg.channel, "orders.1");
130        assert_eq!(msg.event, "OrderUpdated");
131    }
132
133    #[test]
134    fn test_client_message_serialize() {
135        let msg = ClientMessage::Subscribe {
136            channel: "private-orders.1".into(),
137            auth: Some("auth_token".into()),
138            channel_data: None,
139        };
140        let json = serde_json::to_string(&msg).unwrap();
141        assert!(json.contains("subscribe"));
142        assert!(json.contains("private-orders.1"));
143    }
144
145    #[test]
146    fn test_subscribe_with_channel_data() {
147        let json = r#"{"type":"subscribe","channel":"presence-nearby","auth":"ok","channel_data":{"user_id":"42","user_info":{"name":"Alice"}}}"#;
148        let msg: ClientMessage = serde_json::from_str(json).unwrap();
149        match msg {
150            ClientMessage::Subscribe {
151                channel,
152                auth,
153                channel_data,
154            } => {
155                assert_eq!(channel, "presence-nearby");
156                assert_eq!(auth.unwrap(), "ok");
157                let data = channel_data.unwrap();
158                assert_eq!(data["user_id"], "42");
159                assert_eq!(data["user_info"]["name"], "Alice");
160            }
161            _ => panic!("Expected Subscribe"),
162        }
163    }
164
165    #[test]
166    fn test_subscribe_without_channel_data() {
167        let json = r#"{"type":"subscribe","channel":"presence-nearby","auth":"ok"}"#;
168        let msg: ClientMessage = serde_json::from_str(json).unwrap();
169        match msg {
170            ClientMessage::Subscribe { channel_data, .. } => {
171                assert!(channel_data.is_none());
172            }
173            _ => panic!("Expected Subscribe"),
174        }
175    }
176
177    #[test]
178    fn test_server_message_serialize() {
179        let msg = ServerMessage::Connected {
180            socket_id: "abc123".into(),
181        };
182        let json = serde_json::to_string(&msg).unwrap();
183        assert!(json.contains("connected"));
184        assert!(json.contains("abc123"));
185    }
186}