wsforge_core/message.rs
1//! WebSocket message types and utilities.
2//!
3//! This module provides the core message abstraction used throughout WsForge.
4//! It wraps the underlying WebSocket message types from `tokio-tungstenite` and
5//! provides convenient methods for creating, inspecting, and parsing messages.
6//!
7//! # Overview
8//!
9//! WebSocket messages can be one of several types:
10//! - **Text**: UTF-8 encoded string data
11//! - **Binary**: Raw byte data
12//! - **Ping/Pong**: Control frames for keep-alive
13//! - **Close**: Connection termination frames
14//!
15//! The [`Message`] type provides a unified interface for working with all these types,
16//! with automatic conversion between WsForge's message type and the underlying
17//! `tungstenite` message type.
18//!
19//! # Message Types
20//!
21//! | Type | Description | Use Case |
22//! |------|-------------|----------|
23//! | [`MessageType::Text`] | UTF-8 text | JSON, commands, chat messages |
24//! | [`MessageType::Binary`] | Raw bytes | Images, files, protocol buffers |
25//! | [`MessageType::Ping`] | Keep-alive request | Connection health checks |
26//! | [`MessageType::Pong`] | Keep-alive response | Responding to pings |
27//! | [`MessageType::Close`] | Connection close | Graceful disconnection |
28//!
29//! # Examples
30//!
31//! ## Creating Messages
32//!
33//! ```
34//! use wsforge::prelude::*;
35//!
36//! // Text message
37//! let text_msg = Message::text("Hello, WebSocket!");
38//!
39//! // Binary message
40//! let binary_msg = Message::binary(vec![0x01, 0x02, 0x03]);
41//!
42//! // JSON message (text)
43//! let json_msg = Message::text(r#"{"type":"greeting","text":"hello"}"#);
44//! ```
45//!
46//! ## Inspecting Messages
47//!
48//! ```
49//! use wsforge::prelude::*;
50//!
51//! # fn example(msg: Message) {
52//! if msg.is_text() {
53//! println!("Text: {}", msg.as_text().unwrap());
54//! } else if msg.is_binary() {
55//! println!("Binary: {} bytes", msg.as_bytes().len());
56//! }
57//!
58//! // Get the message type
59//! match msg.message_type() {
60//! MessageType::Text => println!("Received text"),
61//! MessageType::Binary => println!("Received binary"),
62//! _ => println!("Received control frame"),
63//! }
64//! # }
65//! ```
66//!
67//! ## Parsing JSON
68//!
69//! ```
70//! use wsforge::prelude::*;
71//! use serde::Deserialize;
72//!
73//! #[derive(Deserialize)]
74//! struct ChatMessage {
75//! username: String,
76//! text: String,
77//! }
78//!
79//! # fn example(msg: Message) -> Result<()> {
80//! // Parse JSON from message
81//! let chat: ChatMessage = msg.json()?;
82//! println!("{} says: {}", chat.username, chat.text);
83//! # Ok(())
84//! # }
85//! ```
86
87use crate::error::Result;
88use serde::de::DeserializeOwned;
89use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
90
91/// Represents the type of a WebSocket message.
92///
93/// This enum categorizes messages into their protocol-defined types.
94/// Most application logic will work with text and binary messages,
95/// while control frames (Ping, Pong, Close) are typically handled
96/// by the framework automatically.
97///
98/// # Examples
99///
100/// ```
101/// use wsforge::prelude::*;
102///
103/// # fn example(msg: Message) {
104/// match msg.message_type() {
105/// MessageType::Text => {
106/// println!("Processing text message");
107/// }
108/// MessageType::Binary => {
109/// println!("Processing binary data");
110/// }
111/// MessageType::Ping => {
112/// println!("Received ping (will auto-respond with pong)");
113/// }
114/// MessageType::Pong => {
115/// println!("Received pong");
116/// }
117/// MessageType::Close => {
118/// println!("Client disconnecting");
119/// }
120/// }
121/// # }
122/// ```
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum MessageType {
125 /// Text message containing UTF-8 encoded string data.
126 ///
127 /// This is the most common message type for:
128 /// - JSON data
129 /// - Plain text chat messages
130 /// - Commands and instructions
131 /// - XML or other text-based formats
132 Text,
133
134 /// Binary message containing raw byte data.
135 ///
136 /// Use this for:
137 /// - Images, audio, video files
138 /// - Protocol buffers, MessagePack
139 /// - Custom binary protocols
140 /// - Large data transfers
141 Binary,
142
143 /// Ping frame for connection keep-alive.
144 ///
145 /// Servers send pings to check if the connection is still active.
146 /// Clients should respond with a Pong frame (handled automatically).
147 Ping,
148
149 /// Pong frame responding to a Ping.
150 ///
151 /// This is automatically sent in response to Ping frames.
152 /// You rarely need to create these manually.
153 Pong,
154
155 /// Close frame indicating connection termination.
156 ///
157 /// Sent when either side wants to close the connection gracefully.
158 /// Contains optional close code and reason.
159 Close,
160}
161
162/// A WebSocket message.
163///
164/// This is the main type for working with WebSocket messages in WsForge.
165/// It wraps the raw message data and provides convenient methods for
166/// creating, inspecting, and converting messages.
167///
168/// # Thread Safety
169///
170/// `Message` is cheaply cloneable and can be safely shared across threads.
171///
172/// # Examples
173///
174/// ## Creating Messages
175///
176/// ```
177/// use wsforge::prelude::*;
178///
179/// // Create text message
180/// let greeting = Message::text("Hello!");
181///
182/// // Create binary message
183/// let data = Message::binary(vec!);[1][2][3][4]
184/// ```
185///
186/// ## Working with Content
187///
188/// ```
189/// use wsforge::prelude::*;
190///
191/// # fn example(msg: Message) {
192/// // Check message type
193/// if msg.is_text() {
194/// // Get text content
195/// if let Some(text) = msg.as_text() {
196/// println!("Message: {}", text);
197/// }
198/// }
199///
200/// // Get raw bytes (works for both text and binary)
201/// let bytes = msg.as_bytes();
202/// println!("Size: {} bytes", bytes.len());
203/// # }
204/// ```
205#[derive(Debug, Clone)]
206pub struct Message {
207 /// The raw message data as bytes.
208 ///
209 /// For text messages, this contains UTF-8 encoded text.
210 /// For binary messages, this contains raw bytes.
211 pub data: Vec<u8>,
212
213 /// The type of this message.
214 pub msg_type: MessageType,
215}
216
217impl Message {
218 /// Creates a new text message.
219 ///
220 /// The string is converted to UTF-8 bytes and stored as a text message.
221 ///
222 /// # Arguments
223 ///
224 /// * `text` - The text content (any type convertible to `String`)
225 ///
226 /// # Examples
227 ///
228 /// ```
229 /// use wsforge::prelude::*;
230 ///
231 /// // From &str
232 /// let msg1 = Message::text("Hello");
233 ///
234 /// // From String
235 /// let msg2 = Message::text(String::from("World"));
236 ///
237 /// // From format!
238 /// let msg3 = Message::text(format!("User {} joined", 42));
239 /// ```
240 pub fn text(text: impl Into<String>) -> Self {
241 let string = text.into();
242 Self {
243 data: string.into_bytes(),
244 msg_type: MessageType::Text,
245 }
246 }
247
248 /// Creates a new binary message.
249 ///
250 /// The bytes are stored as-is without any encoding or processing.
251 ///
252 /// # Arguments
253 ///
254 /// * `data` - The binary data
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use wsforge::prelude::*;
260 ///
261 /// // From Vec<u8>
262 /// let msg1 = Message::binary(vec![0x01, 0x02, 0x03]);
263 ///
264 /// // From byte slice
265 /// let bytes: &[u8] = &[0xFF, 0xFE, 0xFD];
266 /// let msg2 = Message::binary(bytes.to_vec());
267 ///
268 /// // From array
269 /// let msg3 = Message::binary(.to_vec());[2][3][4][1]
270 /// ```
271 pub fn binary(data: Vec<u8>) -> Self {
272 Self {
273 data,
274 msg_type: MessageType::Binary,
275 }
276 }
277
278 /// Creates a ping message.
279 ///
280 /// Ping messages are used for connection keep-alive. The client
281 /// should respond with a pong message (handled automatically by
282 /// most WebSocket implementations).
283 ///
284 /// # Arguments
285 ///
286 /// * `data` - Optional payload data (usually empty)
287 ///
288 /// # Examples
289 ///
290 /// ```
291 /// use wsforge::prelude::*;
292 ///
293 /// // Simple ping
294 /// let ping = Message::ping(vec![]);
295 ///
296 /// // Ping with payload
297 /// let ping_with_data = Message::ping(b"timestamp:1234567890".to_vec());
298 /// ```
299 pub fn ping(data: Vec<u8>) -> Self {
300 Self {
301 data,
302 msg_type: MessageType::Ping,
303 }
304 }
305
306 /// Creates a pong message.
307 ///
308 /// Pong messages are sent in response to ping messages. This is
309 /// typically handled automatically by the framework.
310 ///
311 /// # Arguments
312 ///
313 /// * `data` - Payload data (should match the ping payload)
314 ///
315 /// # Examples
316 ///
317 /// ```
318 /// use wsforge::prelude::*;
319 ///
320 /// # fn example(ping_msg: Message) {
321 /// // Respond to a ping
322 /// if ping_msg.is_ping() {
323 /// let pong = Message::pong(ping_msg.data.clone());
324 /// // Send pong back...
325 /// }
326 /// # }
327 /// ```
328 pub fn pong(data: Vec<u8>) -> Self {
329 Self {
330 data,
331 msg_type: MessageType::Pong,
332 }
333 }
334
335 /// Creates a close message.
336 ///
337 /// Close messages signal graceful connection termination.
338 /// The connection will close after this message is sent/received.
339 ///
340 /// # Examples
341 ///
342 /// ```
343 /// use wsforge::prelude::*;
344 ///
345 /// // Simple close
346 /// let close = Message::close();
347 /// ```
348 pub fn close() -> Self {
349 Self {
350 data: Vec::new(),
351 msg_type: MessageType::Close,
352 }
353 }
354
355 /// Converts this message to a `tungstenite` message.
356 ///
357 /// This is used internally by the framework to convert between
358 /// WsForge's message type and the underlying WebSocket library.
359 ///
360 /// # Examples
361 ///
362 /// ```
363 /// use wsforge::prelude::*;
364 ///
365 /// # fn example() {
366 /// let msg = Message::text("Hello");
367 /// let tungstenite_msg = msg.into_tungstenite();
368 /// # }
369 /// ```
370 pub fn into_tungstenite(self) -> TungsteniteMessage {
371 match self.msg_type {
372 MessageType::Text => {
373 TungsteniteMessage::Text(String::from_utf8_lossy(&self.data).to_string())
374 }
375 MessageType::Binary => TungsteniteMessage::Binary(self.data),
376 MessageType::Ping => TungsteniteMessage::Ping(self.data),
377 MessageType::Pong => TungsteniteMessage::Pong(self.data),
378 MessageType::Close => TungsteniteMessage::Close(None),
379 }
380 }
381
382 /// Creates a message from a `tungstenite` message.
383 ///
384 /// This is used internally by the framework to convert incoming
385 /// WebSocket messages to WsForge's message type.
386 ///
387 /// # Arguments
388 ///
389 /// * `msg` - A tungstenite WebSocket message
390 ///
391 /// # Examples
392 ///
393 /// ```
394 /// use wsforge::prelude::*;
395 /// use tokio_tungstenite::tungstenite::Message as TungsteniteMessage;
396 ///
397 /// # fn example() {
398 /// let tung_msg = TungsteniteMessage::Text("Hello".to_string());
399 /// let msg = Message::from_tungstenite(tung_msg);
400 /// assert!(msg.is_text());
401 /// # }
402 /// ```
403 pub fn from_tungstenite(msg: TungsteniteMessage) -> Self {
404 match msg {
405 TungsteniteMessage::Text(text) => Self::text(text),
406 TungsteniteMessage::Binary(data) => Self::binary(data),
407 TungsteniteMessage::Ping(data) => Self::ping(data),
408 TungsteniteMessage::Pong(data) => Self::pong(data),
409 TungsteniteMessage::Close(_) => Self::close(),
410 TungsteniteMessage::Frame(_) => Self::binary(vec![]),
411 }
412 }
413
414 /// Returns the type of this message.
415 ///
416 /// # Examples
417 ///
418 /// ```
419 /// use wsforge::prelude::*;
420 ///
421 /// # fn example() {
422 /// let msg = Message::text("Hello");
423 /// assert_eq!(msg.message_type(), MessageType::Text);
424 ///
425 /// let binary = Message::binary(vec!);[3][1][2]
426 /// assert_eq!(binary.message_type(), MessageType::Binary);
427 /// # }
428 /// ```
429 pub fn message_type(&self) -> MessageType {
430 self.msg_type
431 }
432
433 /// Checks if this is a text message.
434 ///
435 /// # Examples
436 ///
437 /// ```
438 /// use wsforge::prelude::*;
439 ///
440 /// # fn example(msg: Message) {
441 /// if msg.is_text() {
442 /// println!("Processing text: {}", msg.as_text().unwrap());
443 /// }
444 /// # }
445 /// ```
446 pub fn is_text(&self) -> bool {
447 self.msg_type == MessageType::Text
448 }
449
450 /// Checks if this is a binary message.
451 ///
452 /// # Examples
453 ///
454 /// ```
455 /// use wsforge::prelude::*;
456 ///
457 /// # fn example(msg: Message) {
458 /// if msg.is_binary() {
459 /// let bytes = msg.as_bytes();
460 /// println!("Processing {} bytes of binary data", bytes.len());
461 /// }
462 /// # }
463 /// ```
464 pub fn is_binary(&self) -> bool {
465 self.msg_type == MessageType::Binary
466 }
467
468 /// Checks if this is a ping message.
469 ///
470 /// # Examples
471 ///
472 /// ```
473 /// use wsforge::prelude::*;
474 ///
475 /// # fn example(msg: Message) {
476 /// if msg.is_ping() {
477 /// // Framework usually auto-responds with pong
478 /// println!("Received ping");
479 /// }
480 /// # }
481 /// ```
482 pub fn is_ping(&self) -> bool {
483 self.msg_type == MessageType::Ping
484 }
485
486 /// Checks if this is a pong message.
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// use wsforge::prelude::*;
492 ///
493 /// # fn example(msg: Message) {
494 /// if msg.is_pong() {
495 /// println!("Connection is alive");
496 /// }
497 /// # }
498 /// ```
499 pub fn is_pong(&self) -> bool {
500 self.msg_type == MessageType::Pong
501 }
502
503 /// Checks if this is a close message.
504 ///
505 /// # Examples
506 ///
507 /// ```
508 /// use wsforge::prelude::*;
509 ///
510 /// # fn example(msg: Message) {
511 /// if msg.is_close() {
512 /// println!("Client is disconnecting");
513 /// }
514 /// # }
515 /// ```
516 pub fn is_close(&self) -> bool {
517 self.msg_type == MessageType::Close
518 }
519
520 /// Returns the message content as a string slice, if it's a text message.
521 ///
522 /// Returns `None` if the message is not text or if the data is not valid UTF-8.
523 ///
524 /// # Examples
525 ///
526 /// ```
527 /// use wsforge::prelude::*;
528 ///
529 /// # fn example(msg: Message) {
530 /// if let Some(text) = msg.as_text() {
531 /// println!("Received: {}", text);
532 /// }
533 /// # }
534 /// ```
535 pub fn as_text(&self) -> Option<&str> {
536 if self.is_text() {
537 std::str::from_utf8(&self.data).ok()
538 } else {
539 None
540 }
541 }
542
543 /// Returns the message content as a byte slice.
544 ///
545 /// This works for all message types, returning the raw underlying data.
546 ///
547 /// # Examples
548 ///
549 /// ```
550 /// use wsforge::prelude::*;
551 ///
552 /// # fn example(msg: Message) {
553 /// let bytes = msg.as_bytes();
554 /// println!("Message size: {} bytes", bytes.len());
555 ///
556 /// // Works for text messages too
557 /// let text_msg = Message::text("Hello");
558 /// assert_eq!(text_msg.as_bytes(), b"Hello");
559 /// # }
560 /// ```
561 pub fn as_bytes(&self) -> &[u8] {
562 &self.data
563 }
564
565 /// Deserializes the message content as JSON.
566 ///
567 /// This is a convenience method for parsing JSON from text messages.
568 /// The type must implement `serde::Deserialize`.
569 ///
570 /// # Errors
571 ///
572 /// Returns an error if:
573 /// - The message is not text
574 /// - The JSON is malformed
575 /// - The JSON doesn't match the expected type
576 ///
577 /// # Examples
578 ///
579 /// ## Simple Types
580 ///
581 /// ```
582 /// use wsforge::prelude::*;
583 ///
584 /// # fn example() -> Result<()> {
585 /// let msg = Message::text(r#"{"name":"Alice","age":30}"#);
586 ///
587 /// let value: serde_json::Value = msg.json()?;
588 /// assert_eq!(value["name"], "Alice");
589 /// # Ok(())
590 /// # }
591 /// ```
592 ///
593 /// ## Struct Deserialization
594 ///
595 /// ```
596 /// use wsforge::prelude::*;
597 /// use serde::Deserialize;
598 ///
599 /// #[derive(Deserialize)]
600 /// struct User {
601 /// name: String,
602 /// age: u32,
603 /// }
604 ///
605 /// # fn example() -> Result<()> {
606 /// let msg = Message::text(r#"{"name":"Alice","age":30}"#);
607 /// let user: User = msg.json()?;
608 /// assert_eq!(user.name, "Alice");
609 /// # Ok(())
610 /// # }
611 /// ```
612 pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
613 let text = self
614 .as_text()
615 .ok_or_else(|| crate::error::Error::InvalidMessage)?;
616 Ok(serde_json::from_str(text)?)
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn test_text_message() {
626 let msg = Message::text("Hello, World!");
627 assert!(msg.is_text());
628 assert_eq!(msg.as_text(), Some("Hello, World!"));
629 assert_eq!(msg.message_type(), MessageType::Text);
630 }
631
632 #[test]
633 fn test_binary_message() {
634 let data = vec![1, 2, 3, 4, 5];
635 let msg = Message::binary(data.clone());
636 assert!(msg.is_binary());
637 assert_eq!(msg.as_bytes(), &data[..]);
638 assert_eq!(msg.message_type(), MessageType::Binary);
639 }
640
641 #[test]
642 fn test_ping_message() {
643 let msg = Message::ping(vec![]);
644 assert!(msg.is_ping());
645 assert_eq!(msg.message_type(), MessageType::Ping);
646 }
647
648 #[test]
649 fn test_pong_message() {
650 let msg = Message::pong(vec![]);
651 assert!(msg.is_pong());
652 assert_eq!(msg.message_type(), MessageType::Pong);
653 }
654
655 #[test]
656 fn test_close_message() {
657 let msg = Message::close();
658 assert!(msg.is_close());
659 assert_eq!(msg.message_type(), MessageType::Close);
660 }
661
662 #[test]
663 fn test_json_parsing() {
664 let msg = Message::text(r#"{"key":"value","number":42}"#);
665 let json: serde_json::Value = msg.json().unwrap();
666 assert_eq!(json["key"], "value");
667 assert_eq!(json["number"], 42);
668 }
669
670 #[test]
671 fn test_as_bytes() {
672 let text_msg = Message::text("Hello");
673 assert_eq!(text_msg.as_bytes(), b"Hello");
674
675 let binary_msg = Message::binary(vec![1, 2, 3]);
676 assert_eq!(binary_msg.as_bytes(), &[1, 2, 3]);
677 }
678
679 #[test]
680 fn test_tungstenite_conversion() {
681 let msg = Message::text("test");
682 let tung_msg = msg.clone().into_tungstenite();
683 let back = Message::from_tungstenite(tung_msg);
684 assert_eq!(back.as_text(), msg.as_text());
685 }
686}