use buffa::Message;
use bytes::Bytes;
#[cfg(feature = "json")]
use serde::Serialize;
#[cfg(feature = "json")]
use serde::de::DeserializeOwned;
use crate::error::ConnectError;
pub mod content_type {
pub const PROTO: &str = "application/proto";
pub const JSON: &str = "application/json";
pub const CONNECT_PROTO: &str = "application/connect+proto";
pub const CONNECT_JSON: &str = "application/connect+json";
}
pub mod header {
pub const PROTOCOL_VERSION: &str = "connect-protocol-version";
pub const TIMEOUT_MS: &str = "connect-timeout-ms";
pub const CONTENT_ENCODING: &str = "connect-content-encoding";
pub const ACCEPT_ENCODING: &str = "connect-accept-encoding";
}
#[cfg(feature = "json")]
pub trait JsonSerialize: Serialize {}
#[cfg(feature = "json")]
impl<T: Serialize> JsonSerialize for T {}
#[cfg(not(feature = "json"))]
pub trait JsonSerialize {}
#[cfg(not(feature = "json"))]
impl<T> JsonSerialize for T {}
#[cfg(feature = "json")]
pub trait JsonDeserialize: DeserializeOwned {}
#[cfg(feature = "json")]
impl<T: DeserializeOwned> JsonDeserialize for T {}
#[cfg(not(feature = "json"))]
pub trait JsonDeserialize {}
#[cfg(not(feature = "json"))]
impl<T> JsonDeserialize for T {}
pub fn encode_proto<M: Message>(message: &M) -> Result<Bytes, ConnectError> {
Ok(message.encode_to_bytes())
}
pub fn decode_proto<M: Message>(data: &[u8]) -> Result<M, ConnectError> {
M::decode_from_slice(data)
.map_err(|e| ConnectError::invalid_argument(format!("failed to decode proto: {e}")))
}
#[cfg(not(feature = "json"))]
pub(crate) const JSON_FEATURE_DISABLED: &str =
"JSON codec not compiled in (connectrpc built without the `json` feature)";
#[cfg(feature = "json")]
pub fn encode_json<M: Serialize>(message: &M) -> Result<Bytes, ConnectError> {
serde_json::to_vec(message)
.map(Bytes::from)
.map_err(|e| ConnectError::internal(format!("failed to encode JSON: {e}")))
}
#[cfg(not(feature = "json"))]
pub fn encode_json<M>(_message: &M) -> Result<Bytes, ConnectError> {
Err(ConnectError::unimplemented(JSON_FEATURE_DISABLED))
}
#[cfg(feature = "json")]
pub fn decode_json<M: DeserializeOwned>(data: &[u8]) -> Result<M, ConnectError> {
serde_json::from_slice(data)
.map_err(|e| ConnectError::invalid_argument(format!("failed to decode JSON: {e}")))
}
#[cfg(not(feature = "json"))]
pub fn decode_json<M>(_data: &[u8]) -> Result<M, ConnectError> {
Err(ConnectError::unimplemented(JSON_FEATURE_DISABLED))
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ProtoCodec;
impl ProtoCodec {
pub fn content_type() -> &'static str {
content_type::PROTO
}
pub fn encode<M: Message>(message: &M) -> Result<Bytes, ConnectError> {
encode_proto(message)
}
pub fn decode<M: Message>(data: &[u8]) -> Result<M, ConnectError> {
decode_proto(data)
}
}
#[cfg(feature = "json")]
#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct JsonCodec;
#[cfg(feature = "json")]
impl JsonCodec {
pub fn content_type() -> &'static str {
content_type::JSON
}
pub fn encode<M: Serialize>(message: &M) -> Result<Bytes, ConnectError> {
encode_json(message)
}
pub fn decode<M: DeserializeOwned>(data: &[u8]) -> Result<M, ConnectError> {
decode_json(data)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CodecFormat {
Proto,
Json,
}
impl std::fmt::Display for CodecFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Proto => write!(f, "proto"),
Self::Json => write!(f, "json"),
}
}
}
impl CodecFormat {
pub fn from_content_type(content_type: &str) -> Option<Self> {
if content_type.starts_with(content_type::PROTO)
|| content_type.starts_with(content_type::CONNECT_PROTO)
{
return Some(Self::Proto);
}
#[cfg(feature = "json")]
if content_type.starts_with(content_type::JSON)
|| content_type.starts_with(content_type::CONNECT_JSON)
{
return Some(Self::Json);
}
None
}
pub fn from_codec(codec: &str) -> Option<Self> {
match codec {
"proto" => Some(Self::Proto),
#[cfg(feature = "json")]
"json" => Some(Self::Json),
_ => None,
}
}
#[inline]
pub fn content_type(&self) -> &'static str {
match self {
Self::Proto => content_type::PROTO,
Self::Json => content_type::JSON,
}
}
#[inline]
pub fn streaming_content_type(&self) -> &'static str {
match self {
Self::Proto => content_type::CONNECT_PROTO,
Self::Json => content_type::CONNECT_JSON,
}
}
#[inline]
pub fn is_streaming_content_type(content_type: &str) -> bool {
if content_type.starts_with(content_type::CONNECT_PROTO) {
return true;
}
#[cfg(feature = "json")]
if content_type.starts_with(content_type::CONNECT_JSON) {
return true;
}
false
}
}
#[cfg(test)]
mod tests {
#[cfg(not(feature = "json"))]
#[test]
fn markers_are_empty_bounds_without_json() {
use super::{JsonDeserialize, JsonSerialize};
struct NoSerde;
fn assert_serialize<T: JsonSerialize>() {}
fn assert_deserialize<T: JsonDeserialize>() {}
assert_serialize::<NoSerde>();
assert_deserialize::<NoSerde>();
}
#[cfg(not(feature = "json"))]
#[test]
fn parsers_reject_json_without_feature() {
use super::CodecFormat;
assert_eq!(CodecFormat::from_codec("json"), None);
assert_eq!(CodecFormat::from_codec("proto"), Some(CodecFormat::Proto));
for ct in ["application/json", "application/connect+json"] {
assert_eq!(CodecFormat::from_content_type(ct), None, "{ct}");
}
assert_eq!(
CodecFormat::from_content_type("application/proto"),
Some(CodecFormat::Proto)
);
assert!(!CodecFormat::is_streaming_content_type(
"application/connect+json"
));
assert!(CodecFormat::is_streaming_content_type(
"application/connect+proto"
));
}
#[cfg(feature = "json")]
#[test]
fn parsers_accept_json_with_feature() {
use super::CodecFormat;
assert_eq!(CodecFormat::from_codec("json"), Some(CodecFormat::Json));
assert_eq!(
CodecFormat::from_content_type("application/json"),
Some(CodecFormat::Json)
);
assert!(CodecFormat::is_streaming_content_type(
"application/connect+json"
));
}
}