nautilus_network/transport/
message.rs1use bytes::Bytes;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum Message {
33 Text(Bytes),
35 Binary(Bytes),
37 Ping(Bytes),
39 Pong(Bytes),
41 Close(Option<CloseFrame>),
43}
44
45impl Message {
46 #[inline]
48 #[must_use]
49 pub fn text(s: impl Into<String>) -> Self {
50 Self::Text(Bytes::from(s.into()))
51 }
52
53 #[inline]
59 #[must_use]
60 pub fn as_text(&self) -> Option<&str> {
61 match self {
62 Self::Text(b) => std::str::from_utf8(b).ok(),
63 _ => None,
64 }
65 }
66
67 #[inline]
69 #[must_use]
70 pub fn binary(data: impl Into<Bytes>) -> Self {
71 Self::Binary(data.into())
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn ping(data: impl Into<Bytes>) -> Self {
78 Self::Ping(data.into())
79 }
80
81 #[inline]
83 #[must_use]
84 pub fn pong(data: impl Into<Bytes>) -> Self {
85 Self::Pong(data.into())
86 }
87
88 #[inline]
90 #[must_use]
91 pub const fn is_text(&self) -> bool {
92 matches!(self, Self::Text(_))
93 }
94
95 #[inline]
97 #[must_use]
98 pub const fn is_binary(&self) -> bool {
99 matches!(self, Self::Binary(_))
100 }
101
102 #[inline]
104 #[must_use]
105 pub const fn is_ping(&self) -> bool {
106 matches!(self, Self::Ping(_))
107 }
108
109 #[inline]
111 #[must_use]
112 pub const fn is_pong(&self) -> bool {
113 matches!(self, Self::Pong(_))
114 }
115
116 #[inline]
118 #[must_use]
119 pub const fn is_close(&self) -> bool {
120 matches!(self, Self::Close(_))
121 }
122
123 #[inline]
125 #[must_use]
126 pub const fn is_control(&self) -> bool {
127 matches!(self, Self::Ping(_) | Self::Pong(_) | Self::Close(_))
128 }
129
130 #[inline]
134 #[must_use]
135 pub fn as_bytes(&self) -> &[u8] {
136 match self {
137 Self::Text(b) | Self::Binary(b) | Self::Ping(b) | Self::Pong(b) => b,
138 Self::Close(_) => &[],
139 }
140 }
141
142 #[inline]
146 #[must_use]
147 pub fn into_bytes(self) -> Bytes {
148 match self {
149 Self::Text(b) | Self::Binary(b) | Self::Ping(b) | Self::Pong(b) => b,
150 Self::Close(_) => Bytes::new(),
151 }
152 }
153}
154
155impl From<String> for Message {
156 #[inline]
157 fn from(s: String) -> Self {
158 Self::Text(Bytes::from(s))
159 }
160}
161
162impl From<&str> for Message {
163 #[inline]
164 fn from(s: &str) -> Self {
165 Self::Text(Bytes::copy_from_slice(s.as_bytes()))
166 }
167}
168
169impl From<Vec<u8>> for Message {
170 #[inline]
171 fn from(v: Vec<u8>) -> Self {
172 Self::Binary(Bytes::from(v))
173 }
174}
175
176impl From<Bytes> for Message {
177 #[inline]
178 fn from(b: Bytes) -> Self {
179 Self::Binary(b)
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct CloseFrame {
189 pub code: u16,
191 pub reason: String,
193}
194
195impl CloseFrame {
196 pub const NORMAL: u16 = 1000;
198 pub const GOING_AWAY: u16 = 1001;
200 pub const PROTOCOL_ERROR: u16 = 1002;
202 pub const UNSUPPORTED: u16 = 1003;
204 pub const ABNORMAL: u16 = 1006;
206 pub const POLICY_VIOLATION: u16 = 1008;
208 pub const MESSAGE_TOO_LARGE: u16 = 1009;
210 pub const INTERNAL_ERROR: u16 = 1011;
212
213 #[inline]
215 #[must_use]
216 pub fn new(code: u16, reason: impl Into<String>) -> Self {
217 Self {
218 code,
219 reason: reason.into(),
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use rstest::rstest;
227
228 use super::*;
229
230 #[rstest]
231 fn text_constructor_round_trips() {
232 let msg = Message::text("hello");
233 assert!(msg.is_text());
234 assert_eq!(msg.as_bytes(), b"hello");
235 }
236
237 #[rstest]
238 fn binary_constructor_round_trips() {
239 let msg = Message::binary(vec![1, 2, 3]);
240 assert!(msg.is_binary());
241 assert_eq!(msg.as_bytes(), &[1, 2, 3]);
242 }
243
244 #[rstest]
245 fn ping_pong_classify_as_control() {
246 assert!(Message::ping(Bytes::new()).is_control());
247 assert!(Message::pong(Bytes::new()).is_control());
248 assert!(Message::Close(None).is_control());
249 assert!(!Message::text("x").is_control());
250 assert!(!Message::binary(vec![]).is_control());
251 }
252
253 #[rstest]
254 fn close_frame_carries_code_and_reason() {
255 let frame = CloseFrame::new(CloseFrame::GOING_AWAY, "shutdown");
256 assert_eq!(frame.code, 1001);
257 assert_eq!(frame.reason, "shutdown");
258 }
259
260 #[rstest]
261 fn into_bytes_consumes_payload() {
262 let msg = Message::binary(vec![9, 8, 7]);
263 let bytes = msg.into_bytes();
264 assert_eq!(&bytes[..], &[9, 8, 7]);
265 }
266
267 #[rstest]
268 fn into_bytes_close_returns_empty() {
269 let msg = Message::Close(Some(CloseFrame::new(1000, "bye")));
270 assert!(msg.into_bytes().is_empty());
271 }
272
273 #[rstest]
274 fn as_text_returns_str_for_valid_utf8() {
275 let msg = Message::text("café");
276 assert_eq!(msg.as_text(), Some("café"));
277 }
278
279 #[rstest]
280 fn as_text_returns_none_for_invalid_utf8() {
281 let msg = Message::Text(Bytes::from_static(&[0xFF, 0xFE]));
282 assert!(msg.as_text().is_none());
283 }
284
285 #[rstest]
286 fn as_text_returns_none_for_non_text() {
287 assert!(Message::binary(vec![1u8]).as_text().is_none());
288 assert!(Message::ping(Bytes::new()).as_text().is_none());
289 assert!(Message::Close(None).as_text().is_none());
290 }
291}