use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::error::KafkaError;
pub type Header = (String, Vec<u8>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Record {
topic: String,
key: Option<Vec<u8>>,
payload: Option<Vec<u8>>,
headers: Vec<Header>,
}
impl Record {
#[must_use]
pub fn new(topic: impl Into<String>, payload: impl Into<Vec<u8>>) -> Self {
Self {
topic: topic.into(),
key: None,
payload: Some(payload.into()),
headers: Vec::new(),
}
}
pub fn json<T: Serialize + ?Sized>(
topic: impl Into<String>,
value: &T,
) -> Result<Self, KafkaError> {
let payload = serde_json::to_vec(value)?;
Ok(Self::new(topic, payload).with_header("content-type", "application/json"))
}
#[must_use]
pub fn tombstone(topic: impl Into<String>, key: impl Into<Vec<u8>>) -> Self {
Self {
topic: topic.into(),
key: Some(key.into()),
payload: None,
headers: Vec::new(),
}
}
pub(crate) fn keyless_tombstone(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
key: None,
payload: None,
headers: Vec::new(),
}
}
#[must_use]
pub fn with_key(mut self, key: impl Into<Vec<u8>>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
#[must_use]
pub fn key(&self) -> Option<&[u8]> {
self.key.as_deref()
}
#[must_use]
pub fn payload(&self) -> &[u8] {
self.payload.as_deref().unwrap_or_default()
}
#[must_use]
pub const fn is_tombstone(&self) -> bool {
self.payload.is_none()
}
#[must_use]
pub fn headers(&self) -> &[Header] {
&self.headers
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Delivery {
pub partition: i32,
pub offset: i64,
}
impl Delivery {
#[must_use]
pub const fn new(partition: i32, offset: i64) -> Self {
Self { partition, offset }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
topic: String,
partition: i32,
offset: i64,
key: Option<Vec<u8>>,
payload: Option<Vec<u8>>,
headers: Vec<Header>,
timestamp_ms: Option<i64>,
}
impl Message {
#[must_use]
pub fn new(topic: impl Into<String>, payload: impl Into<Vec<u8>>) -> Self {
Self {
payload: Some(payload.into()),
..Self::tombstone(topic)
}
}
#[must_use]
pub fn tombstone(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
partition: 0,
offset: 0,
key: None,
payload: None,
headers: Vec::new(),
timestamp_ms: None,
}
}
#[must_use]
pub const fn with_position(mut self, partition: i32, offset: i64) -> Self {
self.partition = partition;
self.offset = offset;
self
}
#[must_use]
pub fn with_key(mut self, key: impl Into<Vec<u8>>) -> Self {
self.key = Some(key.into());
self
}
#[must_use]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
#[must_use]
pub const fn with_timestamp_ms(mut self, timestamp_ms: i64) -> Self {
self.timestamp_ms = Some(timestamp_ms);
self
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
#[must_use]
pub const fn partition(&self) -> i32 {
self.partition
}
#[must_use]
pub const fn offset(&self) -> i64 {
self.offset
}
#[must_use]
pub fn key(&self) -> Option<&[u8]> {
self.key.as_deref()
}
#[must_use]
pub fn payload(&self) -> &[u8] {
self.payload.as_deref().unwrap_or_default()
}
#[must_use]
pub const fn is_tombstone(&self) -> bool {
self.payload.is_none()
}
#[must_use]
pub fn headers(&self) -> &[Header] {
&self.headers
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&[u8]> {
self.headers
.iter()
.rev()
.find(|(n, _)| n == name)
.map(|(_, v)| v.as_slice())
}
#[must_use]
pub const fn timestamp_ms(&self) -> Option<i64> {
self.timestamp_ms
}
pub(crate) fn from_record(
record: Record,
partition: i32,
offset: i64,
timestamp_ms: Option<i64>,
) -> Self {
Self {
topic: record.topic,
partition,
offset,
key: record.key,
payload: record.payload,
headers: record.headers,
timestamp_ms,
}
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T, KafkaError> {
Ok(serde_json::from_slice(self.payload())?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Order {
id: u32,
}
#[test]
fn record_new_has_payload_and_no_key() {
let record = Record::new("orders", b"hi".to_vec());
assert_eq!(record.topic(), "orders");
assert_eq!(record.payload(), b"hi");
assert!(!record.is_tombstone());
assert_eq!(record.key(), None);
assert!(record.headers().is_empty());
}
#[test]
fn record_builder_sets_key_and_headers() {
let record = Record::new("t", "p")
.with_key("k")
.with_header("a", "1")
.with_header("b", "2");
assert_eq!(record.key(), Some(&b"k"[..]));
assert_eq!(
record.headers(),
&[
("a".to_owned(), b"1".to_vec()),
("b".to_owned(), b"2".to_vec())
]
);
}
#[test]
fn record_json_encodes_and_sets_content_type() {
let record = Record::json("orders", &Order { id: 7 }).unwrap();
assert_eq!(record.payload(), br#"{"id":7}"#);
assert_eq!(
record.headers(),
&[("content-type".to_owned(), b"application/json".to_vec())]
);
}
#[test]
fn record_tombstone_has_key_and_no_payload() {
let record = Record::tombstone("t", "k");
assert!(record.is_tombstone());
assert_eq!(record.payload(), b"");
assert_eq!(record.key(), Some(&b"k"[..]));
}
#[test]
fn message_builder_sets_all_fields() {
let msg = Message::new("t", "p")
.with_position(3, 42)
.with_key("k")
.with_header("h", "v")
.with_timestamp_ms(1000);
assert_eq!(msg.topic(), "t");
assert_eq!(msg.partition(), 3);
assert_eq!(msg.offset(), 42);
assert_eq!(msg.key(), Some(&b"k"[..]));
assert_eq!(msg.payload(), b"p");
assert!(!msg.is_tombstone());
assert_eq!(msg.headers().len(), 1);
assert_eq!(msg.timestamp_ms(), Some(1000));
}
#[test]
fn message_defaults_are_empty() {
let msg = Message::new("t", "p");
assert_eq!((msg.partition(), msg.offset()), (0, 0));
assert_eq!(msg.key(), None);
assert_eq!(msg.timestamp_ms(), None);
}
#[test]
fn message_tombstone_has_empty_payload() {
let msg = Message::tombstone("t");
assert!(msg.is_tombstone());
assert_eq!(msg.payload(), b"");
}
#[test]
fn message_header_returns_last_value() {
let msg = Message::new("t", "p")
.with_header("h", "1")
.with_header("x", "0")
.with_header("h", "2");
assert_eq!(msg.header("h"), Some(&b"2"[..]));
assert_eq!(msg.header("missing"), None);
}
#[test]
fn message_json_decodes_payload() {
let msg = Message::new("t", r#"{"id":9}"#);
assert_eq!(msg.json::<Order>().unwrap(), Order { id: 9 });
}
#[test]
fn message_json_reports_bad_payload() {
let err = Message::new("t", "nope").json::<Order>().unwrap_err();
assert!(matches!(err, KafkaError::Json(_)), "{err}");
}
}