195
crates/sq-models/src/message.rs
Normal file
195
crates/sq-models/src/message.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use std::fmt;
|
||||
|
||||
/// A single message in the queue.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Message {
|
||||
/// Monotonically increasing within a topic-partition. Assigned by the server.
|
||||
pub offset: u64,
|
||||
/// Topic this message belongs to.
|
||||
pub topic: TopicName,
|
||||
/// Partition within the topic.
|
||||
pub partition: u32,
|
||||
/// Optional partitioning key.
|
||||
pub key: Option<Vec<u8>>,
|
||||
/// The payload.
|
||||
pub value: Vec<u8>,
|
||||
/// User-defined headers (metadata).
|
||||
pub headers: Vec<Header>,
|
||||
/// Server-assigned wall-clock timestamp (millis since epoch).
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
/// A key-value header attached to a message.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Header {
|
||||
pub key: String,
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A topic name wrapper.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct TopicName(pub String);
|
||||
|
||||
impl TopicName {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TopicName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for TopicName {
|
||||
fn from(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for TopicName {
|
||||
fn from(s: String) -> Self {
|
||||
Self(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a closed WAL segment ready for shipping.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClosedSegment {
|
||||
pub path: std::path::PathBuf,
|
||||
pub topic: TopicName,
|
||||
pub partition: u32,
|
||||
pub base_offset: u64,
|
||||
pub end_offset: u64,
|
||||
pub size_bytes: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_message_construction() {
|
||||
let msg = Message {
|
||||
offset: 42,
|
||||
topic: TopicName::from("orders"),
|
||||
partition: 0,
|
||||
key: Some(b"user-123".to_vec()),
|
||||
value: b"hello world".to_vec(),
|
||||
headers: vec![Header {
|
||||
key: "content-type".to_string(),
|
||||
value: b"text/plain".to_vec(),
|
||||
}],
|
||||
timestamp_ms: 1700000000000,
|
||||
};
|
||||
|
||||
assert_eq!(msg.offset, 42);
|
||||
assert_eq!(msg.topic.as_str(), "orders");
|
||||
assert_eq!(msg.partition, 0);
|
||||
assert_eq!(msg.key.as_deref(), Some(b"user-123".as_slice()));
|
||||
assert_eq!(msg.value, b"hello world");
|
||||
assert_eq!(msg.headers.len(), 1);
|
||||
assert_eq!(msg.headers[0].key, "content-type");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_no_key_no_headers() {
|
||||
let msg = Message {
|
||||
offset: 0,
|
||||
topic: TopicName::from("events"),
|
||||
partition: 1,
|
||||
key: None,
|
||||
value: b"payload".to_vec(),
|
||||
headers: vec![],
|
||||
timestamp_ms: 0,
|
||||
};
|
||||
|
||||
assert!(msg.key.is_none());
|
||||
assert!(msg.headers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_clone_eq() {
|
||||
let msg = Message {
|
||||
offset: 1,
|
||||
topic: TopicName::from("test"),
|
||||
partition: 0,
|
||||
key: None,
|
||||
value: b"data".to_vec(),
|
||||
headers: vec![],
|
||||
timestamp_ms: 100,
|
||||
};
|
||||
|
||||
let cloned = msg.clone();
|
||||
assert_eq!(msg, cloned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_topic_name_ordering() {
|
||||
let a = TopicName::from("alpha");
|
||||
let b = TopicName::from("beta");
|
||||
assert!(a < b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_topic_name_display() {
|
||||
let t = TopicName::from("my-topic");
|
||||
assert_eq!(format!("{t}"), "my-topic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_empty_value() {
|
||||
let msg = Message {
|
||||
offset: 0,
|
||||
topic: TopicName::from("t"),
|
||||
partition: 0,
|
||||
key: None,
|
||||
value: vec![],
|
||||
headers: vec![],
|
||||
timestamp_ms: 0,
|
||||
};
|
||||
|
||||
assert!(msg.value.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_large_value() {
|
||||
let large = vec![0xFFu8; 1024 * 1024]; // 1MB
|
||||
let msg = Message {
|
||||
offset: 0,
|
||||
topic: TopicName::from("t"),
|
||||
partition: 0,
|
||||
key: None,
|
||||
value: large.clone(),
|
||||
headers: vec![],
|
||||
timestamp_ms: 0,
|
||||
};
|
||||
|
||||
assert_eq!(msg.value.len(), 1024 * 1024);
|
||||
assert_eq!(msg.value, large);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_many_headers() {
|
||||
let headers: Vec<Header> = (0..100)
|
||||
.map(|i| Header {
|
||||
key: format!("header-{i}"),
|
||||
value: format!("value-{i}").into_bytes(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let msg = Message {
|
||||
offset: 0,
|
||||
topic: TopicName::from("t"),
|
||||
partition: 0,
|
||||
key: None,
|
||||
value: vec![],
|
||||
headers,
|
||||
timestamp_ms: 0,
|
||||
};
|
||||
|
||||
assert_eq!(msg.headers.len(), 100);
|
||||
assert_eq!(msg.headers[99].key, "header-99");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user