use bytes::BufMut;
use bytes::BytesMut;
use cheetah_string::CheetahString;
use super::private::Sealed;
use super::write_json_string;
use super::HeaderFieldContext;
use super::HeaderValue;
use super::ProtocolContractViolation;
use crate::protocol::command_custom_header::HeaderMap;
pub trait EncodeSink: Sealed {
fn write<V: HeaderValue>(
&mut self,
key: &'static str,
value: &V,
context: HeaderFieldContext,
) -> Result<(), ProtocolContractViolation>;
}
pub struct MapSink<'a> {
out: &'a mut HeaderMap,
}
impl<'a> MapSink<'a> {
#[inline]
pub const fn new(out: &'a mut HeaderMap) -> Self {
Self { out }
}
#[inline]
pub fn into_inner(self) -> &'a mut HeaderMap {
self.out
}
}
impl Sealed for MapSink<'_> {}
impl EncodeSink for MapSink<'_> {
#[inline]
fn write<V: HeaderValue>(
&mut self,
key: &'static str,
value: &V,
_context: HeaderFieldContext,
) -> Result<(), ProtocolContractViolation> {
self.out
.insert(CheetahString::from_static_str(key), value.to_map_value());
Ok(())
}
}
pub struct JsonSink<'a> {
out: &'a mut BytesMut,
first: bool,
}
impl<'a> JsonSink<'a> {
#[inline]
pub fn new(out: &'a mut BytesMut) -> Self {
out.extend_from_slice(b"{");
Self { out, first: true }
}
#[inline]
pub fn finish(self) -> &'a mut BytesMut {
self.out.extend_from_slice(b"}");
self.out
}
}
impl Sealed for JsonSink<'_> {}
impl EncodeSink for JsonSink<'_> {
#[inline]
fn write<V: HeaderValue>(
&mut self,
key: &'static str,
value: &V,
_context: HeaderFieldContext,
) -> Result<(), ProtocolContractViolation> {
if self.first {
self.first = false;
} else {
self.out.extend_from_slice(b",");
}
write_json_string(self.out, key);
self.out.extend_from_slice(b":");
value.write_json_string(self.out);
Ok(())
}
}
pub struct BinarySink<'a> {
out: &'a mut BytesMut,
}
impl<'a> BinarySink<'a> {
#[inline]
pub fn new(out: &'a mut BytesMut) -> Self {
Self { out }
}
#[inline]
pub fn into_inner(self) -> &'a mut BytesMut {
self.out
}
}
impl Sealed for BinarySink<'_> {}
impl EncodeSink for BinarySink<'_> {
#[inline]
fn write<V: HeaderValue>(
&mut self,
key: &'static str,
value: &V,
context: HeaderFieldContext,
) -> Result<(), ProtocolContractViolation> {
let key_len = u16::try_from(key.len()).map_err(|_| ProtocolContractViolation::KeyLengthOverflow {
header: context.header,
key: context.key,
})?;
if !V::ALWAYS_FITS_WIRE_LENGTH && value.encoded_len() > i32::MAX as usize {
return Err(ProtocolContractViolation::ValueLengthOverflow {
header: context.header,
key: context.key,
});
}
let pair_start = self.out.len();
self.out.put_u16(key_len);
self.out.extend_from_slice(key.as_bytes());
let value_len_offset = self.out.len();
self.out.put_i32(0);
let value_offset = self.out.len();
value.write_ascii(self.out);
let actual_value_len = self.out.len() - value_offset;
let actual_value_len = i32::try_from(actual_value_len).map_err(|_| {
self.out.truncate(pair_start);
ProtocolContractViolation::ValueLengthOverflow {
header: context.header,
key: context.key,
}
})?;
self.out[value_len_offset..value_len_offset + 4].copy_from_slice(&actual_value_len.to_be_bytes());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::header_codec::HeaderValueKind;
const CONTEXT: HeaderFieldContext =
HeaderFieldContext::new("ExampleHeader", "topic", HeaderValueKind::String, None);
#[test]
fn writes_into_existing_map_without_an_intermediate_map() {
let mut fields = HeaderMap::with_capacity(2);
fields.insert(
CheetahString::from_static_str("existing"),
CheetahString::from_static_str("value"),
);
let mut sink = MapSink::new(&mut fields);
sink.write("topic", &CheetahString::from("测试-topic"), CONTEXT)
.unwrap();
let fields = sink.into_inner();
assert_eq!(fields.len(), 2);
assert_eq!(fields.get("topic").map(CheetahString::as_str), Some("测试-topic"));
assert_eq!(fields.get("existing").map(CheetahString::as_str), Some("value"));
}
#[test]
fn canonical_write_replaces_an_existing_value() {
let mut fields = HeaderMap::new();
fields.insert(
CheetahString::from_static_str("topic"),
CheetahString::from_static_str("old"),
);
MapSink::new(&mut fields)
.write("topic", &String::from("new"), CONTEXT)
.unwrap();
assert_eq!(fields.get("topic").map(CheetahString::as_str), Some("new"));
}
#[test]
fn scalar_write_uses_the_header_value_canonical_form() {
let mut fields = HeaderMap::new();
let mut sink = MapSink::new(&mut fields);
sink.write(
"queueOffset",
&u64::MAX,
HeaderFieldContext::new("ExampleHeader", "queueOffset", HeaderValueKind::U64, None),
)
.unwrap();
assert_eq!(
fields.get("queueOffset").map(CheetahString::as_str),
Some("18446744073709551615")
);
}
#[test]
fn binary_sink_appends_canonical_pairs_without_replacing_existing_bytes() {
let mut out = BytesMut::from(&b"prefix"[..]);
let mut sink = BinarySink::new(&mut out);
sink.write("queueOffset", &-42_i64, CONTEXT).unwrap();
let out = sink.into_inner();
assert_eq!(out.len() - 6, 2 + 11 + 4 + 3);
assert_eq!(&out[..6], b"prefix");
assert_eq!(u16::from_be_bytes(out[6..8].try_into().unwrap()), 11);
assert_eq!(&out[8..19], b"queueOffset");
assert_eq!(i32::from_be_bytes(out[19..23].try_into().unwrap()), 3);
assert_eq!(&out[23..], b"-42");
}
#[test]
fn binary_sink_rejects_an_oversized_key_without_mutating_the_destination() {
let key = Box::leak("k".repeat(u16::MAX as usize + 1).into_boxed_str());
let mut out = BytesMut::from(&b"prefix"[..]);
let error = BinarySink::new(&mut out).write(key, &true, CONTEXT).unwrap_err();
assert!(matches!(error, ProtocolContractViolation::KeyLengthOverflow { .. }));
assert_eq!(out.as_ref(), b"prefix");
}
#[test]
fn json_sink_writes_string_scalars_and_escapes_text_without_allocating_values() {
let mut out = BytesMut::new();
let mut sink = JsonSink::new(&mut out);
sink.write("topic", &CheetahString::from("主题\"\\\n"), CONTEXT)
.unwrap();
sink.write(
"queueOffset",
&-42_i64,
HeaderFieldContext::new("ExampleHeader", "queueOffset", HeaderValueKind::I64, None),
)
.unwrap();
sink.write(
"enabled",
&true,
HeaderFieldContext::new("ExampleHeader", "enabled", HeaderValueKind::Bool, None),
)
.unwrap();
sink.finish();
let value: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(value["topic"], "主题\"\\\n");
assert_eq!(value["queueOffset"], "-42");
assert_eq!(value["enabled"], "true");
}
}