1use crate::error::{WsError, WsResult};
11use crate::frame::{CloseCode, CloseFrame, Frame, OpCode};
12use rustlavel_core::Json;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Message {
16 Text(String),
17 Binary(Vec<u8>),
18 Ping(Vec<u8>),
19 Pong(Vec<u8>),
20 Close(Option<CloseFrame>),
22}
23
24impl Message {
25 pub fn text(body: impl Into<String>) -> Message {
26 Message::Text(body.into())
27 }
28
29 pub fn json(value: &Json) -> Message {
31 Message::Text(value.to_string())
32 }
33
34 pub fn close(code: CloseCode, reason: impl Into<String>) -> Message {
35 Message::Close(Some(CloseFrame::new(code, reason)))
36 }
37
38 pub fn as_text(&self) -> Option<&str> {
39 match self {
40 Message::Text(body) => Some(body),
41 _ => None,
42 }
43 }
44
45 pub fn as_bytes(&self) -> &[u8] {
46 match self {
47 Message::Text(body) => body.as_bytes(),
48 Message::Binary(bytes) | Message::Ping(bytes) | Message::Pong(bytes) => bytes,
49 Message::Close(_) => &[],
50 }
51 }
52
53 pub fn is_data(&self) -> bool {
55 matches!(self, Message::Text(_) | Message::Binary(_))
56 }
57
58 pub fn len(&self) -> usize {
59 self.as_bytes().len()
60 }
61
62 pub fn is_empty(&self) -> bool {
63 self.len() == 0
64 }
65
66 pub fn kind(&self) -> &'static str {
69 match self {
70 Message::Text(_) => "text",
71 Message::Binary(_) => "binary",
72 Message::Ping(_) => "ping",
73 Message::Pong(_) => "pong",
74 Message::Close(_) => "close",
75 }
76 }
77
78 pub fn into_frame(self) -> Frame {
80 match self {
81 Message::Text(body) => Frame::server(OpCode::Text, body.into_bytes()),
82 Message::Binary(bytes) => Frame::server(OpCode::Binary, bytes),
83 Message::Ping(bytes) => Frame::server(OpCode::Ping, bytes),
84 Message::Pong(bytes) => Frame::server(OpCode::Pong, bytes),
85 Message::Close(close) => Frame::server(
86 OpCode::Close,
87 close.map(|close| close.to_payload()).unwrap_or_default(),
88 ),
89 }
90 }
91
92 pub fn from_data(opcode: OpCode, payload: Vec<u8>) -> WsResult<Message> {
98 match opcode {
99 OpCode::Text => {
100 String::from_utf8(payload).map(Message::Text).map_err(|_| WsError::InvalidUtf8)
101 }
102 OpCode::Binary => Ok(Message::Binary(payload)),
103 other => Err(WsError::protocol(format!("{other:?} is not a data frame"))),
104 }
105 }
106}
107
108impl From<String> for Message {
109 fn from(body: String) -> Message {
110 Message::Text(body)
111 }
112}
113
114impl From<&str> for Message {
115 fn from(body: &str) -> Message {
116 Message::Text(body.to_string())
117 }
118}
119
120impl From<Json> for Message {
121 fn from(value: Json) -> Message {
122 Message::Text(value.to_string())
123 }
124}
125
126impl From<Vec<u8>> for Message {
127 fn from(bytes: Vec<u8>) -> Message {
128 Message::Binary(bytes)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn a_text_message_becomes_an_unmasked_text_frame() {
138 let frame = Message::text("hi").into_frame();
139
140 assert_eq!(frame.opcode, OpCode::Text);
141 assert!(frame.fin);
142 assert_eq!(frame.mask, None, "a server must never mask");
143 assert_eq!(frame.encode(), b"\x81\x02hi");
144 }
145
146 #[test]
147 fn a_close_message_encodes_its_code_and_reason() {
148 let frame = Message::close(CloseCode::POLICY, "no").into_frame();
149 assert_eq!(frame.encode(), b"\x88\x04\x03\xf0no");
150
151 assert_eq!(Message::Close(None).into_frame().encode(), b"\x88\x00");
153 }
154
155 #[test]
156 fn reassembled_text_must_be_valid_utf8() {
157 assert_eq!(
158 Message::from_data(OpCode::Text, "héllo".as_bytes().to_vec()).unwrap(),
159 Message::text("héllo")
160 );
161
162 let error = Message::from_data(OpCode::Text, vec![0xf0, 0x28, 0x8c, 0x28]).unwrap_err();
164 assert_eq!(error.close_code(), CloseCode::INVALID_PAYLOAD);
165 }
166
167 #[test]
168 fn binary_payloads_are_passed_through_untouched() {
169 let bytes = vec![0x00, 0xff, 0x80];
170 assert_eq!(
171 Message::from_data(OpCode::Binary, bytes.clone()).unwrap(),
172 Message::Binary(bytes)
173 );
174 }
175
176 #[test]
177 fn instrumentation_sees_a_kind_and_a_size_but_never_the_payload() {
178 let message = Message::text("a secret token");
179
180 assert_eq!(message.kind(), "text");
181 assert_eq!(message.len(), 14);
182 assert!(message.is_data());
183 assert!(!Message::Ping(Vec::new()).is_data());
184 }
185
186 #[test]
187 fn json_values_convert_straight_into_messages() {
188 let value = Json::object([("event", Json::from("ping"))]);
189 assert_eq!(Message::json(&value).as_text(), Some(r#"{"event":"ping"}"#));
190 }
191}