use std::str::FromStr;
use titan_api_types::ws::v1;
use crate::{
codec::{Codec, CodecLoadError, TypedDecoder, TypedEncoder, WrappedDecoder, WrappedEncoder},
dec::{messagepack::MessagePackDecoder, DecodeError, Decoder},
enc::{messagepack::MessagePackEncoder, EncodeError, Encoder},
transform::{
BrotliCompressor, BrotliDecompressor, GzipCompressor, GzipDecompressor, ZstdCompressor,
ZstdDecompressor,
},
};
#[derive(Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ClientCodec {
#[default]
Uncompressed,
#[cfg(feature = "zstd")]
Zstd,
#[cfg(feature = "brotli")]
Brotli,
#[cfg(feature = "gzip")]
Gzip,
}
#[derive(Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum ServerCodec {
#[default]
Uncompressed,
#[cfg(feature = "zstd")]
Zstd,
#[cfg(feature = "brotli")]
Brotli,
#[cfg(feature = "gzip")]
Gzip,
}
impl FromStr for ClientCodec {
type Err = CodecLoadError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
v1::WEBSOCKET_SUBPROTO_BASE => Ok(Self::Uncompressed),
#[cfg(feature = "zstd")]
v1::WEBSOCKET_SUBPROTO_ZSTD => Ok(Self::Zstd),
#[cfg(not(feature = "zstd"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("zstd")),
#[cfg(feature = "brotli")]
v1::WEBSOCKET_SUBPROTO_BROTLI => Ok(Self::Brotli),
#[cfg(not(feature = "brotli"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("brotli")),
#[cfg(feature = "gzip")]
v1::WEBSOCKET_SUBPROTO_GZIP => Ok(Self::Gzip),
#[cfg(not(feature = "gzip"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("gzip")),
_ => Err(CodecLoadError::UnsupportedProtocol(s.into())),
}
}
}
impl Codec for ClientCodec {
type SendItem = v1::ClientRequest;
type SendError = EncodeError;
type RecvItem = v1::ServerMessage;
type RecvError = DecodeError;
fn encoder(
&self,
) -> Box<dyn TypedEncoder<Self::SendItem, Error = Self::SendError> + Send + Sync> {
match self {
Self::Uncompressed => Box::new(WrappedEncoder::new(MessagePackEncoder::default())),
#[cfg(feature = "zstd")]
Self::Zstd => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(ZstdCompressor::default()),
)),
#[cfg(feature = "brotli")]
Self::Brotli => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(BrotliCompressor::default()),
)),
#[cfg(feature = "gzip")]
Self::Gzip => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(GzipCompressor::default()),
)),
}
}
fn decoder(
&self,
) -> Box<dyn TypedDecoder<Item = Self::RecvItem, Error = Self::RecvError> + Send + Sync> {
match self {
Self::Uncompressed => Box::new(WrappedDecoder::new(MessagePackDecoder::default())),
#[cfg(feature = "zstd")]
Self::Zstd => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(ZstdDecompressor::default()),
)),
#[cfg(feature = "brotli")]
Self::Brotli => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(BrotliDecompressor::default()),
)),
#[cfg(feature = "gzip")]
Self::Gzip => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(GzipDecompressor::default()),
)),
}
}
}
impl FromStr for ServerCodec {
type Err = CodecLoadError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
v1::WEBSOCKET_SUBPROTO_BASE => Ok(Self::Uncompressed),
#[cfg(feature = "zstd")]
v1::WEBSOCKET_SUBPROTO_ZSTD => Ok(Self::Zstd),
#[cfg(not(feature = "zstd"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("zstd")),
#[cfg(feature = "brotli")]
v1::WEBSOCKET_SUBPROTO_BROTLI => Ok(Self::Brotli),
#[cfg(not(feature = "brotli"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("brotli")),
#[cfg(feature = "gzip")]
v1::WEBSOCKET_SUBPROTO_GZIP => Ok(Self::Gzip),
#[cfg(not(feature = "gzip"))]
v1::WEBSOCKET_SUBPROTO_ZSTD => Err(CodecLoadError::DisabledEncoding("gzip")),
_ => Err(CodecLoadError::UnsupportedProtocol(s.into())),
}
}
}
impl Codec for ServerCodec {
type SendItem = v1::ServerMessage;
type SendError = EncodeError;
type RecvItem = v1::ClientRequest;
type RecvError = DecodeError;
fn encoder(
&self,
) -> Box<dyn TypedEncoder<Self::SendItem, Error = Self::SendError> + Send + Sync> {
match self {
Self::Uncompressed => Box::new(WrappedEncoder::new(MessagePackEncoder::default())),
#[cfg(feature = "zstd")]
Self::Zstd => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(ZstdCompressor::default()),
)),
#[cfg(feature = "brotli")]
Self::Brotli => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(BrotliCompressor::default()),
)),
#[cfg(feature = "gzip")]
Self::Gzip => Box::new(WrappedEncoder::new(
MessagePackEncoder::default().transform(GzipCompressor::default()),
)),
}
}
fn decoder(
&self,
) -> Box<dyn TypedDecoder<Item = Self::RecvItem, Error = Self::RecvError> + Send + Sync> {
match self {
Self::Uncompressed => Box::new(WrappedDecoder::new(MessagePackDecoder::default())),
#[cfg(feature = "zstd")]
Self::Zstd => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(ZstdDecompressor::default()),
)),
#[cfg(feature = "brotli")]
Self::Brotli => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(BrotliDecompressor::default()),
)),
#[cfg(feature = "gzip")]
Self::Gzip => Box::new(WrappedDecoder::new(
MessagePackDecoder::default().transformed(GzipDecompressor::default()),
)),
}
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use titan_api_types::ws::v1;
use crate::codec::{Codec, CodecLoadError};
use super::{ClientCodec, ServerCodec};
#[test]
fn construct_client_codec_base() {
let codec = ClientCodec::from_str(v1::WEBSOCKET_SUBPROTO_BASE)
.expect("should construct base codec");
assert_eq!(codec, ClientCodec::Uncompressed);
}
#[cfg(feature = "zstd")]
#[test]
fn construct_client_codec_zstd() {
let codec = ClientCodec::from_str(v1::WEBSOCKET_SUBPROTO_ZSTD)
.expect("should construct zstd codec");
assert_eq!(codec, ClientCodec::Zstd);
}
#[cfg(feature = "brotli")]
#[test]
fn construct_client_codec_brotli() {
let codec = ClientCodec::from_str(v1::WEBSOCKET_SUBPROTO_BROTLI)
.expect("should construct brotli codec");
assert_eq!(codec, ClientCodec::Brotli);
}
#[cfg(feature = "gzip")]
#[test]
fn construct_client_codec_gzip() {
let codec = ClientCodec::from_str(v1::WEBSOCKET_SUBPROTO_GZIP)
.expect("should construct gzip codec");
assert_eq!(codec, ClientCodec::Gzip);
}
#[test]
fn construct_client_codec_invalid() {
let err =
ClientCodec::from_str("invalid").expect_err("should have errored on invalid protocol");
assert_eq!(err, CodecLoadError::UnsupportedProtocol("invalid".into()))
}
#[test]
fn construct_server_codec_base() {
let codec = ServerCodec::from_str(v1::WEBSOCKET_SUBPROTO_BASE)
.expect("should construct base codec");
assert_eq!(codec, ServerCodec::Uncompressed);
}
#[cfg(feature = "zstd")]
#[test]
fn construct_server_codec_zstd() {
let codec = ServerCodec::from_str(v1::WEBSOCKET_SUBPROTO_ZSTD)
.expect("should construct zstd codec");
assert_eq!(codec, ServerCodec::Zstd);
}
#[cfg(feature = "brotli")]
#[test]
fn construct_server_codec_brotli() {
let codec = ServerCodec::from_str(v1::WEBSOCKET_SUBPROTO_BROTLI)
.expect("should construct brotli codec");
assert_eq!(codec, ServerCodec::Brotli);
}
#[cfg(feature = "gzip")]
#[test]
fn construct_server_codec_gzip() {
let codec = ServerCodec::from_str(v1::WEBSOCKET_SUBPROTO_GZIP)
.expect("should construct gzip codec");
assert_eq!(codec, ServerCodec::Gzip);
}
#[test]
fn construct_server_codec_invalid() {
let err =
ServerCodec::from_str("invalid").expect_err("should have errored on invalid protocol");
assert_eq!(err, CodecLoadError::UnsupportedProtocol("invalid".into()))
}
#[test]
fn roundtrip_base() {
let client_codec = ClientCodec::Uncompressed;
let mut encoder = client_codec.encoder();
let server_codec = ServerCodec::Uncompressed;
let mut decoder = server_codec.decoder();
let request = v1::ClientRequest {
id: 1,
data: v1::RequestData::GetInfo(v1::GetInfoRequest::default()),
};
let encoded = encoder
.encode_mut(&request)
.expect("should encode client request");
let decoded = decoder
.decode_mut(encoded)
.expect("should decode client request");
assert_eq!(request, decoded);
}
}