rusty_common/websocket/
message.rs

1//! WebSocket message types
2//!
3//! Provides a unified message type that wraps yawc's FrameView.
4
5use smartstring::alias::String;
6use std::fmt;
7use yawc::close::CloseCode;
8use yawc::frame::{FrameView, OpCode};
9
10/// WebSocket message type
11///
12/// Note: This enum holds owned data (String, Vec<u8>) which prevents true zero-copy
13/// operations with memory pools. For high-performance scenarios requiring zero-copy,
14/// consider using the lower-level yawc::FrameView directly or implementing a custom
15/// message type that holds buffer handles instead of owned data.
16#[derive(Clone)]
17pub enum Message {
18    /// Text message
19    Text(String),
20
21    /// Binary message
22    Binary(Vec<u8>),
23
24    /// Ping message
25    Ping(Vec<u8>),
26
27    /// Pong message
28    Pong(Vec<u8>),
29
30    /// Close message
31    Close(Option<(u16, String)>),
32
33    /// Raw frame (for compatibility)
34    Frame(FrameView),
35}
36
37impl Message {
38    /// Create a text message
39    pub fn text<S: Into<String>>(text: S) -> Self {
40        Message::Text(text.into())
41    }
42
43    /// Create a binary message
44    pub fn binary<V: Into<Vec<u8>>>(data: V) -> Self {
45        Message::Binary(data.into())
46    }
47
48    /// Create a ping message
49    pub fn ping<V: Into<Vec<u8>>>(data: V) -> Self {
50        Message::Ping(data.into())
51    }
52
53    /// Create a pong message
54    pub fn pong<V: Into<Vec<u8>>>(data: V) -> Self {
55        Message::Pong(data.into())
56    }
57
58    /// Create a close message
59    #[must_use]
60    pub fn close(code: u16, reason: &str) -> Self {
61        Message::Close(Some((code, reason.into())))
62    }
63
64    /// Check if this is a text message
65    pub const fn is_text(&self) -> bool {
66        matches!(self, Message::Text(_))
67    }
68
69    /// Check if this is a binary message
70    pub const fn is_binary(&self) -> bool {
71        matches!(self, Message::Binary(_))
72    }
73
74    /// Check if this is a ping message
75    pub const fn is_ping(&self) -> bool {
76        matches!(self, Message::Ping(_))
77    }
78
79    /// Check if this is a pong message
80    pub const fn is_pong(&self) -> bool {
81        matches!(self, Message::Pong(_))
82    }
83
84    /// Check if this is a close message
85    pub const fn is_close(&self) -> bool {
86        matches!(self, Message::Close(_))
87    }
88
89    /// Check if this is a frame message
90    pub const fn is_frame(&self) -> bool {
91        matches!(self, Message::Frame(_))
92    }
93
94    /// Get the text content if this is a text message
95    pub fn as_text(&self) -> Option<&str> {
96        match self {
97            Message::Text(text) => Some(text.as_str()),
98            _ => None,
99        }
100    }
101
102    /// Get the binary content if this is a binary message
103    pub fn as_binary(&self) -> Option<&[u8]> {
104        match self {
105            Message::Binary(data) => Some(data),
106            _ => None,
107        }
108    }
109
110    /// Convert to yawc FrameView
111    pub fn to_frame_view(self) -> FrameView {
112        match self {
113            Message::Text(text) => FrameView::text(text.to_string()),
114            Message::Binary(data) => FrameView::binary(data),
115            Message::Ping(data) => FrameView::ping(data),
116            Message::Pong(data) => FrameView::pong(data),
117            Message::Close(Some((code, reason))) => {
118                let close_code = CloseCode::Iana(code);
119                FrameView::close(close_code, reason.as_str())
120            }
121            Message::Close(None) => FrameView::close(CloseCode::Iana(1000), ""),
122            Message::Frame(frame) => frame,
123        }
124    }
125
126    /// Create from yawc FrameView
127    #[must_use]
128    pub fn from_frame_view(frame: FrameView) -> Self {
129        match frame.opcode {
130            OpCode::Text => {
131                let text = std::str::from_utf8(&frame.payload)
132                    .unwrap_or_default()
133                    .into();
134                Message::Text(text)
135            }
136            OpCode::Binary => Message::Binary(frame.payload.to_vec()),
137            OpCode::Ping => Message::Ping(frame.payload.to_vec()),
138            OpCode::Pong => Message::Pong(frame.payload.to_vec()),
139            OpCode::Close => {
140                if frame.payload.len() >= 2 {
141                    let code = u16::from_be_bytes([frame.payload[0], frame.payload[1]]);
142                    let reason = if frame.payload.len() > 2 {
143                        std::str::from_utf8(&frame.payload[2..])
144                            .unwrap_or_default()
145                            .into()
146                    } else {
147                        String::new()
148                    };
149                    Message::Close(Some((code, reason)))
150                } else {
151                    Message::Close(None)
152                }
153            }
154            _ => Message::Binary(vec![]), // Unknown opcodes
155        }
156    }
157}
158
159/// Message type classification
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum MessageType {
162    /// Data message (text or binary)
163    Data,
164
165    /// Control message (ping, pong, close)
166    Control,
167}
168
169impl Message {
170    /// Get the message type
171    pub const fn message_type(&self) -> MessageType {
172        match self {
173            Message::Text(_) | Message::Binary(_) => MessageType::Data,
174            Message::Ping(_) | Message::Pong(_) | Message::Close(_) | Message::Frame(_) => {
175                MessageType::Control
176            }
177        }
178    }
179}
180
181impl fmt::Debug for Message {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        match self {
184            Message::Text(text) => f.debug_tuple("Text").field(text).finish(),
185            Message::Binary(data) => f
186                .debug_tuple("Binary")
187                .field(&format!("{} bytes", data.len()))
188                .finish(),
189            Message::Ping(data) => f
190                .debug_tuple("Ping")
191                .field(&format!("{} bytes", data.len()))
192                .finish(),
193            Message::Pong(data) => f
194                .debug_tuple("Pong")
195                .field(&format!("{} bytes", data.len()))
196                .finish(),
197            Message::Close(frame) => f.debug_tuple("Close").field(frame).finish(),
198            Message::Frame(_) => f.debug_struct("Frame").finish_non_exhaustive(),
199        }
200    }
201}
202
203impl fmt::Display for Message {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Message::Text(text) => write!(f, "Text message: {text}"),
207            Message::Binary(data) => write!(f, "Binary message: {} bytes", data.len()),
208            Message::Ping(data) => write!(f, "Ping message: {} bytes", data.len()),
209            Message::Pong(data) => write!(f, "Pong message: {} bytes", data.len()),
210            Message::Close(Some((code, reason))) => {
211                write!(f, "Close message: {code} - {reason}")
212            }
213            Message::Close(None) => write!(f, "Close message"),
214            Message::Frame(_) => write!(f, "Frame message"),
215        }
216    }
217}