use std::{
fmt::{self, Display},
ops::Deref,
str::Utf8Error,
};
use bytes::Bytes;
use tokio_tungstenite::tungstenite;
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub enum Message {
Text(Utf8Bytes),
Binary(Bytes),
Ping(Bytes),
Pong(Bytes),
Close(Option<CloseFrame>),
}
impl Message {
pub fn text(text: impl Into<Utf8Bytes>) -> Self {
Self::Text(text.into())
}
pub fn binary(data: impl Into<Bytes>) -> Self {
Self::Binary(data.into())
}
#[must_use]
pub fn into_data(self) -> Bytes {
match self {
Self::Text(text) => text.into(),
Self::Binary(data) | Self::Ping(data) | Self::Pong(data) => data,
Self::Close(frame) => frame.map_or_else(Bytes::new, |frame| frame.reason.into()),
}
}
pub fn into_text(self) -> Result<Utf8Bytes, Utf8Error> {
match self {
Self::Text(text) => Ok(text),
message => message.into_data().try_into(),
}
}
pub(crate) fn into_tungstenite(self) -> tungstenite::Message {
match self {
Self::Text(text) => tungstenite::Message::Text(text.0),
Self::Binary(data) => tungstenite::Message::Binary(data),
Self::Ping(data) => tungstenite::Message::Ping(data),
Self::Pong(data) => tungstenite::Message::Pong(data),
Self::Close(frame) => {
tungstenite::Message::Close(frame.map(|frame| tungstenite::protocol::CloseFrame {
code: frame.code.into(),
reason: frame.reason.0,
}))
}
}
}
pub(crate) fn from_tungstenite(message: tungstenite::Message) -> Option<Self> {
match message {
tungstenite::Message::Text(text) => Some(Self::Text(Utf8Bytes(text))),
tungstenite::Message::Binary(data) => Some(Self::Binary(data)),
tungstenite::Message::Ping(data) => Some(Self::Ping(data)),
tungstenite::Message::Pong(data) => Some(Self::Pong(data)),
tungstenite::Message::Close(frame) => {
Some(Self::Close(frame.map(|frame| CloseFrame {
code: frame.code.into(),
reason: Utf8Bytes(frame.reason),
})))
}
tungstenite::Message::Frame(_) => None,
}
}
}
impl From<String> for Message {
fn from(text: String) -> Self {
Self::Text(text.into())
}
}
impl From<&str> for Message {
fn from(text: &str) -> Self {
Self::Text(text.into())
}
}
impl From<Vec<u8>> for Message {
fn from(data: Vec<u8>) -> Self {
Self::Binary(data.into())
}
}
impl From<Bytes> for Message {
fn from(data: Bytes) -> Self {
Self::Binary(data)
}
}
#[derive(Debug, Clone, Default)]
pub struct Utf8Bytes(tungstenite::Utf8Bytes);
impl Utf8Bytes {
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Deref for Utf8Bytes {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl AsRef<str> for Utf8Bytes {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Display for Utf8Bytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(self.as_str(), f)
}
}
impl PartialEq for Utf8Bytes {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for Utf8Bytes {}
impl PartialEq<str> for Utf8Bytes {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for Utf8Bytes {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<String> for Utf8Bytes {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl From<String> for Utf8Bytes {
fn from(text: String) -> Self {
Self(text.into())
}
}
impl From<&str> for Utf8Bytes {
fn from(text: &str) -> Self {
Self(text.into())
}
}
impl TryFrom<Bytes> for Utf8Bytes {
type Error = Utf8Error;
fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
Ok(Self(bytes.try_into()?))
}
}
impl TryFrom<Vec<u8>> for Utf8Bytes {
type Error = Utf8Error;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
Ok(Self(bytes.try_into()?))
}
}
impl From<Utf8Bytes> for Bytes {
fn from(text: Utf8Bytes) -> Self {
text.0.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseFrame {
pub code: CloseCode,
pub reason: Utf8Bytes,
}
pub type CloseCode = u16;
pub mod close_code {
use super::CloseCode;
pub const NORMAL: CloseCode = 1000;
pub const AWAY: CloseCode = 1001;
pub const PROTOCOL: CloseCode = 1002;
pub const UNSUPPORTED: CloseCode = 1003;
pub const INVALID: CloseCode = 1007;
pub const POLICY: CloseCode = 1008;
pub const SIZE: CloseCode = 1009;
pub const EXTENSION: CloseCode = 1010;
pub const ERROR: CloseCode = 1011;
pub const RESTART: CloseCode = 1012;
pub const AGAIN: CloseCode = 1013;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_message_converts_from_strings() {
assert_eq!(Message::text("hi"), Message::from("hi"));
assert_eq!(Message::text("hi"), Message::from(String::from("hi")));
assert_eq!(Message::text("hi"), Message::Text("hi".into()));
}
#[test]
fn binary_message_converts_from_bytes() {
assert_eq!(Message::binary(vec![1, 2]), Message::from(vec![1, 2]));
assert_eq!(
Message::binary(vec![1, 2]),
Message::from(Bytes::from_static(&[1, 2]))
);
}
#[test]
fn into_data_returns_the_payload() {
assert_eq!(Message::text("hi").into_data(), Bytes::from_static(b"hi"));
assert_eq!(
Message::binary(vec![1, 2]).into_data(),
Bytes::from_static(&[1, 2])
);
assert_eq!(Message::Close(None).into_data(), Bytes::new());
assert_eq!(
Message::Close(Some(CloseFrame {
code: close_code::NORMAL,
reason: "done".into(),
}))
.into_data(),
Bytes::from_static(b"done")
);
}
#[test]
fn into_text_validates_utf8() {
assert_eq!(Message::text("hi").into_text().unwrap(), "hi");
assert_eq!(Message::binary(b"hi".to_vec()).into_text().unwrap(), "hi");
assert!(Message::binary(vec![0xff]).into_text().is_err());
}
#[test]
fn utf8_bytes_rejects_invalid_utf8() {
assert!(Utf8Bytes::try_from(vec![0xff]).is_err());
assert!(Utf8Bytes::try_from(Bytes::from_static(&[0xff])).is_err());
assert_eq!(Utf8Bytes::try_from(b"hi".to_vec()).unwrap(), "hi");
}
#[test]
fn close_frame_roundtrips_through_tungstenite() {
let close = Message::Close(Some(CloseFrame {
code: close_code::AWAY,
reason: "bye".into(),
}));
let roundtripped = Message::from_tungstenite(close.clone().into_tungstenite());
assert_eq!(roundtripped, Some(close));
}
#[test]
fn raw_frames_are_not_surfaced() {
let frame = tungstenite::Message::Frame(tungstenite::protocol::frame::Frame::pong(
Bytes::from_static(b"hi"),
));
assert_eq!(Message::from_tungstenite(frame), None);
}
}