Skip to main content

rainier_websocket/
message.rs

1//! What travels over a socket — [`Message`].
2
3use serde::Serialize;
4use serde_json::Value;
5
6use rainier_support::{Error, Result};
7
8/// One WebSocket frame's payload.
9///
10/// Ping and pong are **not** here. They are keep-alive, the transport answers
11/// them, and a handler that had to remember to reply to a ping would have a
12/// connection that dies when it forgets.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Message {
15    /// A text frame. UTF-8 by definition of the protocol.
16    Text(String),
17    /// A binary frame.
18    Binary(Vec<u8>),
19    /// The peer is closing, with an optional reason.
20    ///
21    /// Delivered to [`on_message`](crate::WebSocketHandler::on_message) only if
22    /// a handler asked for it; [`on_close`](crate::WebSocketHandler::on_close)
23    /// is the usual place.
24    Close(Option<String>),
25}
26
27impl Message {
28    /// A text frame.
29    pub fn text(body: impl Into<String>) -> Self {
30        Message::Text(body.into())
31    }
32
33    /// A binary frame.
34    pub fn binary(body: impl Into<Vec<u8>>) -> Self {
35        Message::Binary(body.into())
36    }
37
38    /// A text frame holding `value` as JSON — how nearly every application
39    /// actually talks over a socket.
40    pub fn json(value: &impl Serialize) -> Result<Self> {
41        serde_json::to_string(value)
42            .map(Message::Text)
43            .map_err(|e| Error::internal(format!("a socket message must serialise: {e}")))
44    }
45
46    /// The text, if this is a text frame.
47    pub fn as_text(&self) -> Option<&str> {
48        match self {
49            Message::Text(text) => Some(text),
50            _ => None,
51        }
52    }
53
54    /// The bytes, whichever kind of frame this is.
55    pub fn as_bytes(&self) -> &[u8] {
56        match self {
57            Message::Text(text) => text.as_bytes(),
58            Message::Binary(bytes) => bytes,
59            Message::Close(reason) => reason.as_deref().unwrap_or("").as_bytes(),
60        }
61    }
62
63    /// Parse a text frame as JSON.
64    ///
65    /// A **bad request**, not an internal error: the bytes came from a client,
66    /// and a client that sends nonsense is not a bug in your handler.
67    pub fn parse_json(&self) -> Result<Value> {
68        let text = self
69            .as_text()
70            .ok_or_else(|| Error::bad_request("expected a text frame, got a binary one."))?;
71
72        serde_json::from_str(text)
73            .map_err(|e| Error::bad_request(format!("that is not valid JSON: {e}")))
74    }
75
76    /// Whether this is the peer closing.
77    pub fn is_close(&self) -> bool {
78        matches!(self, Message::Close(_))
79    }
80
81    /// How many bytes the payload is.
82    pub fn len(&self) -> usize {
83        self.as_bytes().len()
84    }
85
86    /// Whether the payload is empty.
87    pub fn is_empty(&self) -> bool {
88        self.len() == 0
89    }
90}
91
92impl From<&str> for Message {
93    fn from(text: &str) -> Self {
94        Message::text(text)
95    }
96}
97
98impl From<String> for Message {
99    fn from(text: String) -> Self {
100        Message::Text(text)
101    }
102}
103
104impl From<Vec<u8>> for Message {
105    fn from(bytes: Vec<u8>) -> Self {
106        Message::Binary(bytes)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn json_round_trips_through_a_text_frame() {
116        let message = Message::json(&serde_json::json!({ "room": 1 })).unwrap();
117
118        assert!(matches!(message, Message::Text(_)));
119        assert_eq!(message.parse_json().unwrap()["room"], 1);
120    }
121
122    #[test]
123    fn nonsense_from_a_client_is_a_bad_request_not_a_500() {
124        let err = Message::text("{not json").parse_json().unwrap_err();
125        assert_eq!(err.status(), 400, "{}", err.message());
126    }
127
128    #[test]
129    fn asking_a_binary_frame_for_json_says_which_kind_it_was() {
130        let err = Message::binary(vec![1, 2, 3]).parse_json().unwrap_err();
131        assert!(err.message().contains("binary"), "{}", err.message());
132    }
133}