use bytes::Bytes;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Message {
Text(Bytes),
Binary(Bytes),
Ping(Bytes),
Pong(Bytes),
Close(Option<CloseFrame>),
}
impl Message {
#[inline]
#[must_use]
pub fn text(s: impl Into<String>) -> Self {
Self::Text(Bytes::from(s.into()))
}
#[inline]
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(b) => std::str::from_utf8(b).ok(),
_ => None,
}
}
#[inline]
#[must_use]
pub fn binary(data: impl Into<Bytes>) -> Self {
Self::Binary(data.into())
}
#[inline]
#[must_use]
pub fn ping(data: impl Into<Bytes>) -> Self {
Self::Ping(data.into())
}
#[inline]
#[must_use]
pub fn pong(data: impl Into<Bytes>) -> Self {
Self::Pong(data.into())
}
#[inline]
#[must_use]
pub const fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
#[inline]
#[must_use]
pub const fn is_binary(&self) -> bool {
matches!(self, Self::Binary(_))
}
#[inline]
#[must_use]
pub const fn is_ping(&self) -> bool {
matches!(self, Self::Ping(_))
}
#[inline]
#[must_use]
pub const fn is_pong(&self) -> bool {
matches!(self, Self::Pong(_))
}
#[inline]
#[must_use]
pub const fn is_close(&self) -> bool {
matches!(self, Self::Close(_))
}
#[inline]
#[must_use]
pub const fn is_control(&self) -> bool {
matches!(self, Self::Ping(_) | Self::Pong(_) | Self::Close(_))
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Text(b) | Self::Binary(b) | Self::Ping(b) | Self::Pong(b) => b,
Self::Close(_) => &[],
}
}
#[inline]
#[must_use]
pub fn into_bytes(self) -> Bytes {
match self {
Self::Text(b) | Self::Binary(b) | Self::Ping(b) | Self::Pong(b) => b,
Self::Close(_) => Bytes::new(),
}
}
}
impl From<String> for Message {
#[inline]
fn from(s: String) -> Self {
Self::Text(Bytes::from(s))
}
}
impl From<&str> for Message {
#[inline]
fn from(s: &str) -> Self {
Self::Text(Bytes::copy_from_slice(s.as_bytes()))
}
}
impl From<Vec<u8>> for Message {
#[inline]
fn from(v: Vec<u8>) -> Self {
Self::Binary(Bytes::from(v))
}
}
impl From<Bytes> for Message {
#[inline]
fn from(b: Bytes) -> Self {
Self::Binary(b)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseFrame {
pub code: u16,
pub reason: String,
}
impl CloseFrame {
pub const NORMAL: u16 = 1000;
pub const GOING_AWAY: u16 = 1001;
pub const PROTOCOL_ERROR: u16 = 1002;
pub const UNSUPPORTED: u16 = 1003;
pub const ABNORMAL: u16 = 1006;
pub const POLICY_VIOLATION: u16 = 1008;
pub const MESSAGE_TOO_LARGE: u16 = 1009;
pub const INTERNAL_ERROR: u16 = 1011;
#[inline]
#[must_use]
pub fn new(code: u16, reason: impl Into<String>) -> Self {
Self {
code,
reason: reason.into(),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn text_constructor_round_trips() {
let msg = Message::text("hello");
assert!(msg.is_text());
assert_eq!(msg.as_bytes(), b"hello");
}
#[rstest]
fn binary_constructor_round_trips() {
let msg = Message::binary(vec![1, 2, 3]);
assert!(msg.is_binary());
assert_eq!(msg.as_bytes(), &[1, 2, 3]);
}
#[rstest]
fn ping_pong_classify_as_control() {
assert!(Message::ping(Bytes::new()).is_control());
assert!(Message::pong(Bytes::new()).is_control());
assert!(Message::Close(None).is_control());
assert!(!Message::text("x").is_control());
assert!(!Message::binary(vec![]).is_control());
}
#[rstest]
fn close_frame_carries_code_and_reason() {
let frame = CloseFrame::new(CloseFrame::GOING_AWAY, "shutdown");
assert_eq!(frame.code, 1001);
assert_eq!(frame.reason, "shutdown");
}
#[rstest]
fn into_bytes_consumes_payload() {
let msg = Message::binary(vec![9, 8, 7]);
let bytes = msg.into_bytes();
assert_eq!(&bytes[..], &[9, 8, 7]);
}
#[rstest]
fn into_bytes_close_returns_empty() {
let msg = Message::Close(Some(CloseFrame::new(1000, "bye")));
assert!(msg.into_bytes().is_empty());
}
#[rstest]
fn as_text_returns_str_for_valid_utf8() {
let msg = Message::text("café");
assert_eq!(msg.as_text(), Some("café"));
}
#[rstest]
fn as_text_returns_none_for_invalid_utf8() {
let msg = Message::Text(Bytes::from_static(&[0xFF, 0xFE]));
assert!(msg.as_text().is_none());
}
#[rstest]
fn as_text_returns_none_for_non_text() {
assert!(Message::binary(vec![1u8]).as_text().is_none());
assert!(Message::ping(Bytes::new()).as_text().is_none());
assert!(Message::Close(None).as_text().is_none());
}
}