use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
#[error("bitemporal retention: {field} — {reason}")]
pub struct RetentionValidationError {
pub field: String,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BitemporalRetention {
pub data_retain_ms: u64,
pub audit_retain_ms: u64,
pub minimum_audit_retain_ms: u64,
}
impl BitemporalRetention {
pub const fn retain_forever() -> Self {
Self {
data_retain_ms: 0,
audit_retain_ms: 0,
minimum_audit_retain_ms: 0,
}
}
pub fn validate(&self) -> Result<(), RetentionValidationError> {
if self.minimum_audit_retain_ms > 0
&& self.audit_retain_ms > 0
&& self.audit_retain_ms < self.minimum_audit_retain_ms
{
return Err(RetentionValidationError {
field: "audit_retain_ms".into(),
reason: format!(
"{} is below minimum_audit_retain_ms ({})",
self.audit_retain_ms, self.minimum_audit_retain_ms
),
});
}
Ok(())
}
}
impl Default for BitemporalRetention {
fn default() -> Self {
Self::retain_forever()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retain_forever_validates() {
assert!(BitemporalRetention::retain_forever().validate().is_ok());
}
#[test]
fn audit_below_floor_rejected() {
let r = BitemporalRetention {
data_retain_ms: 0,
audit_retain_ms: 60_000,
minimum_audit_retain_ms: 120_000,
};
let err = r.validate().expect_err("must reject");
assert_eq!(err.field, "audit_retain_ms");
}
#[test]
fn audit_at_or_above_floor_ok() {
let r = BitemporalRetention {
data_retain_ms: 0,
audit_retain_ms: 120_000,
minimum_audit_retain_ms: 120_000,
};
assert!(r.validate().is_ok());
}
#[test]
fn zero_audit_ignores_floor() {
let r = BitemporalRetention {
data_retain_ms: 0,
audit_retain_ms: 0,
minimum_audit_retain_ms: 120_000,
};
assert!(r.validate().is_ok());
}
#[test]
fn data_and_audit_are_independent() {
let r = BitemporalRetention {
data_retain_ms: 30_000,
audit_retain_ms: 300_000,
minimum_audit_retain_ms: 0,
};
assert!(r.validate().is_ok());
}
}