use std::collections::BTreeMap;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ConnectRecord<K, V> {
pub key: Option<K>,
pub value: Option<V>,
pub timestamp: Option<i64>,
pub headers: Vec<Header>,
}
impl<K, V> ConnectRecord<K, V> {
pub fn new(key: Option<K>, value: Option<V>) -> Self {
Self {
key,
value,
timestamp: None,
headers: Vec::new(),
}
}
#[must_use]
pub fn with_timestamp(mut self, timestamp_ms: i64) -> Self {
self.timestamp = Some(timestamp_ms);
self
}
#[must_use]
pub fn with_header(mut self, key: impl Into<String>, value: Option<Bytes>) -> Self {
self.headers.push(Header {
key: key.into(),
value,
});
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Header {
pub key: String,
pub value: Option<Bytes>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SourceOffset {
pub partition: OffsetMap,
pub position: OffsetMap,
}
impl SourceOffset {
#[must_use]
pub fn new(partition: OffsetMap, position: OffsetMap) -> Self {
Self {
partition,
position,
}
}
}
pub type OffsetMap = BTreeMap<String, OffsetValue>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OffsetValue {
Null,
Bool(bool),
Long(i64),
Double(f64),
String(String),
}
impl From<i64> for OffsetValue {
fn from(v: i64) -> Self {
OffsetValue::Long(v)
}
}
impl From<bool> for OffsetValue {
fn from(v: bool) -> Self {
OffsetValue::Bool(v)
}
}
impl From<f64> for OffsetValue {
fn from(v: f64) -> Self {
OffsetValue::Double(v)
}
}
impl From<String> for OffsetValue {
fn from(v: String) -> Self {
OffsetValue::String(v)
}
}
impl From<&str> for OffsetValue {
fn from(v: &str) -> Self {
OffsetValue::String(v.to_owned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::check;
#[test]
fn builder_sets_timestamp_and_headers() {
let r = ConnectRecord::new(
Some(Bytes::from_static(b"k")),
Some(Bytes::from_static(b"v")),
)
.with_timestamp(42)
.with_header("h", Some(Bytes::from_static(b"hv")));
check!(r.timestamp == Some(42));
check!(r.headers.len() == 1);
check!(r.headers[0].key == "h");
check!(r.headers[0].value == Some(Bytes::from_static(b"hv")));
}
#[test]
fn tombstone_value_is_none() {
let r: ConnectRecord<Bytes, Bytes> =
ConnectRecord::new(Some(Bytes::from_static(b"k")), None);
check!(r.value.is_none());
}
#[test]
fn source_offset_round_trips_through_json() {
let mut partition = OffsetMap::new();
partition.insert("table".into(), "public.orders".into());
let mut position = OffsetMap::new();
position.insert("lsn".into(), 12_345_i64.into());
position.insert("snapshot".into(), OffsetValue::Bool(false));
let offset = SourceOffset::new(partition, position);
let json = serde_json::to_string(&offset).expect("serialize");
let back: SourceOffset = serde_json::from_str(&json).expect("deserialize");
check!(back == offset);
}
#[test]
fn offset_value_from_conversions() {
check!(OffsetValue::from(7_i64) == OffsetValue::Long(7));
check!(OffsetValue::from(true) == OffsetValue::Bool(true));
check!(OffsetValue::from(1.5_f64) == OffsetValue::Double(1.5));
check!(OffsetValue::from("owned".to_owned()) == OffsetValue::String("owned".into()));
check!(OffsetValue::from("borrowed") == OffsetValue::String("borrowed".into()));
}
#[test]
fn offset_map_is_byte_stable_across_insertion_order() {
let mut a = OffsetMap::new();
a.insert("b".into(), 2_i64.into());
a.insert("a".into(), 1_i64.into());
let mut b = OffsetMap::new();
b.insert("a".into(), 1_i64.into());
b.insert("b".into(), 2_i64.into());
check!(serde_json::to_string(&a).unwrap() == serde_json::to_string(&b).unwrap());
}
}