use crate::types::{Point, WalEntry};
use std::fmt;
const WAL_BINARY_V1: u8 = 0x01;
const DISC_WRITE: u8 = 0x00;
const _RETIRED_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);
}
}
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 })
}
d => Err(CodecError::UnknownDiscriminant(d)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{FieldValue, SeriesKey};
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 a_decimal_field_round_trips_through_the_wal() {
let key = SeriesKey::new("meter", BTreeMap::new()).unwrap();
let fields = BTreeMap::from([
(
"z1nb".to_string(),
FieldValue::Decimal("1234.5678".parse().unwrap()),
),
(
"tiny".to_string(),
FieldValue::Decimal("0.30000000000000004".parse().unwrap()),
),
(
"widest".to_string(),
FieldValue::Decimal("99999999999999999999999999999999999999".parse().unwrap()),
),
]);
let entry = WalEntry::Write {
point: Point::new(key, fields, 1).unwrap(),
};
let decoded = decode(&encode(&entry).unwrap()).unwrap();
assert_eq!(entry, decoded);
let WalEntry::Write { point } = decoded;
match point.field("z1nb") {
Some(FieldValue::Decimal(d)) => {
assert_eq!(d.to_string(), "1234.5678");
assert_eq!(d.scale(), 4, "the scale travels with the value");
}
other => panic!("expected a decimal, got {other:?}"),
}
match point.field("tiny") {
Some(FieldValue::Decimal(d)) => assert_eq!(d.to_string(), "0.30000000000000004"),
other => panic!("expected a decimal, got {other:?}"),
}
}
#[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 the_retired_delete_discriminant_is_not_reused() {
let old = [WAL_BINARY_V1, 0x01, 0x00];
assert!(
matches!(decode(&old), Err(CodecError::UnknownDiscriminant(0x01))),
"a retired record must fail loudly"
);
}
#[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());
}
}