use crate::types::{Point, WalEntry};
use std::fmt;
const WAL_BINARY_V1: u8 = 0x01;
const DISC_WRITE: u8 = 0x00;
const DISC_DELETE: u8 = 0x01;
#[derive(Debug)]
pub enum CodecError {
Truncated,
UnknownVersion(u8),
UnknownDiscriminant(u8),
Postcard(postcard::Error),
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Truncated => write!(f, "WAL record truncated"),
Self::UnknownVersion(v) => write!(f, "unknown WAL version prefix: 0x{v:02x}"),
Self::UnknownDiscriminant(d) => write!(f, "unknown WAL binary discriminant: 0x{d:02x}"),
Self::Postcard(e) => write!(f, "WAL postcard error: {e}"),
}
}
}
impl std::error::Error for CodecError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Postcard(e) => Some(e),
_ => None,
}
}
}
impl From<postcard::Error> for CodecError {
fn from(e: postcard::Error) -> Self {
Self::Postcard(e)
}
}
pub fn encode(entry: &WalEntry) -> Result<Vec<u8>, CodecError> {
let mut buf = Vec::with_capacity(256);
buf.push(WAL_BINARY_V1);
match entry {
WalEntry::Write { point } => {
buf.push(DISC_WRITE);
let data = postcard::to_stdvec(point)?;
buf.extend_from_slice(&data);
}
WalEntry::Delete { tombstones } => {
buf.push(DISC_DELETE);
let data = postcard::to_stdvec(tombstones)?;
buf.extend_from_slice(&data);
}
}
Ok(buf)
}
pub fn encode_write_point(point: &Point) -> Result<Vec<u8>, CodecError> {
let mut buf = Vec::with_capacity(256);
buf.push(WAL_BINARY_V1);
buf.push(DISC_WRITE);
let data = postcard::to_stdvec(point)?;
buf.extend_from_slice(&data);
Ok(buf)
}
#[allow(clippy::indexing_slicing)] pub fn decode(data: &[u8]) -> Result<WalEntry, CodecError> {
if data.is_empty() {
return Err(CodecError::Truncated);
}
if data[0] != WAL_BINARY_V1 {
return Err(CodecError::UnknownVersion(data[0]));
}
if data.len() < 2 {
return Err(CodecError::Truncated);
}
let payload = &data[2..];
match data[1] {
DISC_WRITE => {
let point: Point = postcard::from_bytes(payload)?;
Ok(WalEntry::Write { point })
}
DISC_DELETE => {
let tombstones: Vec<crate::types::Tombstone> = postcard::from_bytes(payload)?;
Ok(WalEntry::Delete { tombstones })
}
d => Err(CodecError::UnknownDiscriminant(d)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{FieldValue, SeriesKey, Tombstone};
use std::collections::BTreeMap;
fn sample_point() -> Point {
let key = SeriesKey::new("cpu", BTreeMap::from([("host".into(), "srv-1".into())])).unwrap();
let fields = BTreeMap::from([("value".to_string(), FieldValue::F64(42.5))]);
Point::new(key, fields, 1_000_000_000).unwrap()
}
#[test]
fn write_roundtrip() {
let entry = WalEntry::Write {
point: sample_point(),
};
let encoded = encode(&entry).unwrap();
assert_eq!(encoded[0], WAL_BINARY_V1);
assert_eq!(encoded[1], DISC_WRITE);
let decoded = decode(&encoded).unwrap();
assert_eq!(entry, decoded);
}
#[test]
fn delete_roundtrip_preserves_the_time_range() {
let entry = WalEntry::Delete {
tombstones: vec![
Tombstone::ranged("cpu\0host=srv-1", 100, 200),
Tombstone::ranged("cpu\0host=srv-2", i64::MIN, 5_000).with_segments([1, 2, 3]),
],
};
let encoded = encode(&entry).unwrap();
assert_eq!(encoded[1], DISC_DELETE);
let decoded = decode(&encoded).unwrap();
assert_eq!(entry, decoded);
}
#[test]
fn decode_json_rejected_with_unknown_version() {
let json = b"{\"Write\":{}}";
let err = decode(json).unwrap_err();
assert!(
matches!(err, CodecError::UnknownVersion(0x7B)),
"expected UnknownVersion(0x7B), got {err:?}"
);
}
#[test]
fn decode_empty_payload_returns_error() {
assert!(decode(&[]).is_err());
}
#[test]
fn decode_truncated_binary_returns_error() {
assert!(decode(&[WAL_BINARY_V1]).is_err());
}
#[test]
fn decode_unknown_discriminant_returns_error() {
assert!(decode(&[WAL_BINARY_V1, 0xFF, 0x00]).is_err());
}
}