use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
pub const MAX_INLINE_SIZE: usize = 65_536;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KvEntry {
pub key: String,
pub value: Vec<u8>,
pub content_hash: [u8; 32],
pub content_type: String,
pub metadata: HashMap<String, String>,
pub created_at: u64,
pub updated_at: u64,
}
impl KvEntry {
#[must_use]
pub fn new(key: String, value: Vec<u8>, content_type: String) -> Self {
let content_hash = *blake3::hash(&value).as_bytes();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
Self {
key,
value,
content_hash,
content_type,
metadata: HashMap::new(),
created_at: now,
updated_at: now,
}
}
pub fn update_value(&mut self, value: Vec<u8>, content_type: String) {
self.content_hash = *blake3::hash(&value).as_bytes();
self.value = value;
self.content_type = content_type;
self.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
}
pub fn merge(&mut self, other: &Self) {
if other.updated_at > self.updated_at
|| (other.updated_at == self.updated_at && other.content_hash > self.content_hash)
{
self.value = other.value.clone();
self.content_hash = other.content_hash;
self.content_type = other.content_type.clone();
self.metadata = other.metadata.clone();
self.updated_at = other.updated_at;
if other.created_at < self.created_at {
self.created_at = other.created_at;
}
}
}
#[must_use]
pub fn is_inline(&self) -> bool {
!self.value.is_empty()
}
#[must_use]
pub fn size(&self) -> usize {
self.value.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_entry() {
let entry = KvEntry::new(
"key1".to_string(),
b"hello".to_vec(),
"text/plain".to_string(),
);
assert_eq!(entry.key, "key1");
assert_eq!(entry.value, b"hello");
assert_eq!(entry.content_type, "text/plain");
assert!(entry.is_inline());
assert_eq!(entry.size(), 5);
assert!(entry.created_at > 0);
assert_eq!(entry.created_at, entry.updated_at);
}
#[test]
fn test_content_hash_deterministic() {
let e1 = KvEntry::new("k".to_string(), b"data".to_vec(), "text/plain".to_string());
let e2 = KvEntry::new("k".to_string(), b"data".to_vec(), "text/plain".to_string());
assert_eq!(e1.content_hash, e2.content_hash);
}
#[test]
fn test_content_hash_changes_with_value() {
let e1 = KvEntry::new("k".to_string(), b"aaa".to_vec(), "text/plain".to_string());
let e2 = KvEntry::new("k".to_string(), b"bbb".to_vec(), "text/plain".to_string());
assert_ne!(e1.content_hash, e2.content_hash);
}
#[test]
fn test_update_value() {
let mut entry = KvEntry::new(
"key1".to_string(),
b"old".to_vec(),
"text/plain".to_string(),
);
let old_hash = entry.content_hash;
entry.update_value(b"new".to_vec(), "application/json".to_string());
assert_eq!(entry.value, b"new");
assert_eq!(entry.content_type, "application/json");
assert_ne!(entry.content_hash, old_hash);
assert!(entry.updated_at >= entry.created_at);
}
#[test]
fn test_merge_newer_wins() {
let mut older = KvEntry::new("k".to_string(), b"old".to_vec(), "text/plain".to_string());
older.updated_at = 100;
let mut newer = KvEntry::new("k".to_string(), b"new".to_vec(), "text/plain".to_string());
newer.updated_at = 200;
older.merge(&newer);
assert_eq!(older.value, b"new");
assert_eq!(older.updated_at, 200);
}
#[test]
fn test_merge_older_loses() {
let mut newer = KvEntry::new("k".to_string(), b"new".to_vec(), "text/plain".to_string());
newer.updated_at = 200;
let mut older = KvEntry::new("k".to_string(), b"old".to_vec(), "text/plain".to_string());
older.updated_at = 100;
newer.merge(&older);
assert_eq!(newer.value, b"new"); assert_eq!(newer.updated_at, 200);
}
#[test]
fn test_merge_tie_broken_by_hash() {
let mut e1 = KvEntry::new("k".to_string(), b"aaa".to_vec(), "text/plain".to_string());
e1.updated_at = 100;
let mut e2 = KvEntry::new("k".to_string(), b"zzz".to_vec(), "text/plain".to_string());
e2.updated_at = 100;
let e2_hash = e2.content_hash;
e1.merge(&e2);
if e2_hash > *blake3::hash(b"aaa").as_bytes() {
assert_eq!(e1.value, b"zzz");
}
}
#[test]
fn test_serialization_roundtrip() {
let entry = KvEntry::new(
"key1".to_string(),
b"hello".to_vec(),
"text/plain".to_string(),
);
let bytes = bincode::serialize(&entry).expect("serialize");
let restored: KvEntry = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(entry.key, restored.key);
assert_eq!(entry.value, restored.value);
assert_eq!(entry.content_hash, restored.content_hash);
assert_eq!(entry.content_type, restored.content_type);
}
}