use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct KvEntry {
#[serde(with = "crate::encoding::bin_bytes")]
pub key: Vec<u8>,
#[serde(with = "crate::encoding::bin_bytes")]
pub value: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_micros: Option<u64>,
#[serde(default, skip_serializing_if = "is_zero_u64")]
pub version: u64,
}
fn is_zero_u64(value: &u64) -> bool {
*value == 0
}
impl KvEntry {
pub fn key_str(&self) -> Option<&str> {
std::str::from_utf8(&self.key).ok()
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct KvPage {
pub entries: Vec<KvEntry>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub cursor: Option<Vec<u8>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvGet {
pub v: u32,
pub namespace: String,
#[serde(with = "crate::encoding::bin_bytes")]
pub key: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvSet {
pub v: u32,
pub namespace: String,
#[serde(with = "crate::encoding::bin_bytes")]
pub key: Vec<u8>,
#[serde(with = "crate::encoding::bin_bytes")]
pub value: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_micros: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CasExpect {
Match(u64),
Absent,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvCas {
pub v: u32,
pub namespace: String,
#[serde(with = "crate::encoding::bin_bytes")]
pub key: Vec<u8>,
#[serde(with = "crate::encoding::bin_bytes")]
pub value: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at_micros: Option<u64>,
pub expect: CasExpect,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvDelete {
pub v: u32,
pub namespace: String,
#[serde(with = "crate::encoding::bin_bytes")]
pub key: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvNamespaces {
pub v: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct KvNamespaceInfo {
pub namespace: String,
pub entries: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvScan {
pub v: u32,
pub namespace: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub prefix: Option<Vec<u8>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub start: Option<Vec<u8>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub end: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_contains: Option<String>,
pub limit: usize,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub cursor: Option<Vec<u8>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvDeleteMany {
pub v: u32,
pub namespace: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub prefix: Option<Vec<u8>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub start: Option<Vec<u8>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "crate::encoding::opt_bin_bytes"
)]
pub end: Option<Vec<u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key_contains: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum KvReply {
Ok(KvOutcome),
Err(KvError),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum KvOutcome {
Value(Option<KvEntry>),
Written,
Committed { version: u64 },
Deleted(bool),
DeletedMany(usize),
Page(KvPage),
Namespaces(Vec<KvNamespaceInfo>),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[non_exhaustive]
pub enum KvError {
#[error("kv not supported: {0}")]
Unsupported(String),
#[error("invalid key: {0}")]
InvalidKey(String),
#[error("{what} is {size}B, exceeds cap {cap}B")]
TooLarge {
what: String,
size: usize,
cap: usize,
},
#[error("kv backend error: {0}")]
Backend(String),
#[error("unsupported kv op version (expected {expected}, got {got})")]
Version { expected: u32, got: u32 },
#[error("kv version conflict (current: {current:?})")]
VersionConflict { current: Option<u64> },
}
#[cfg(all(test, feature = "cbor"))]
mod tests {
use super::*;
use crate::codes::KV_OP_VERSION;
use crate::framing::{decode_named, encode_named};
#[test]
fn given_kv_delete_many_when_round_tripped_then_should_preserve_bounds() {
let request = KvDeleteMany {
v: KV_OP_VERSION,
namespace: "sessions".to_owned(),
prefix: Some(b"user:".to_vec()),
start: None,
end: None,
key_contains: Some("stale".to_owned()),
};
let bytes = encode_named(&request).expect("serializes");
let back: KvDeleteMany = decode_named(&bytes).expect("deserializes");
assert_eq!(back.prefix, Some(b"user:".to_vec()));
assert_eq!(back.key_contains.as_deref(), Some("stale"));
}
#[test]
fn given_kv_deleted_many_reply_when_round_tripped_then_should_preserve_count() {
let reply = KvReply::Ok(KvOutcome::DeletedMany(7));
let bytes = encode_named(&reply).expect("serializes");
let back: KvReply = decode_named(&bytes).expect("deserializes");
match back {
KvReply::Ok(KvOutcome::DeletedMany(n)) => assert_eq!(n, 7),
other => panic!("expected DeletedMany, got {other:?}"),
}
}
#[test]
fn given_a_set_request_when_round_tripped_then_should_preserve_value_and_expiry() {
let request = KvSet {
v: KV_OP_VERSION,
namespace: "sessions".to_owned(),
key: b"user:42".to_vec(),
value: b"online".to_vec(),
expires_at_micros: Some(1_700_000_000_000_000),
};
let bytes = encode_named(&request).expect("the request serializes");
let back: KvSet = decode_named(&bytes).expect("the request deserializes");
assert_eq!(back.key, b"user:42");
assert_eq!(back.value, b"online");
assert_eq!(back.expires_at_micros, Some(1_700_000_000_000_000));
}
#[test]
fn given_a_binary_key_entry_when_round_tripped_then_should_preserve_raw_bytes() {
let reply = KvReply::Ok(KvOutcome::Value(Some(KvEntry {
key: vec![0xff, 0x00, 0xfe],
value: vec![0x00, 0x01, 0x02],
expires_at_micros: None,
version: 0,
})));
let bytes = encode_named(&reply).expect("the reply serializes");
let back: KvReply = decode_named(&bytes).expect("the reply deserializes");
let KvReply::Ok(KvOutcome::Value(Some(entry))) = back else {
panic!("expected an Ok(Value(Some)) reply");
};
assert_eq!(entry.key, vec![0xff, 0x00, 0xfe]);
assert_eq!(entry.key_str(), None, "non-UTF-8 key has no string form");
assert_eq!(entry.value, vec![0x00, 0x01, 0x02]);
}
#[test]
fn given_a_scan_page_when_round_tripped_then_should_preserve_cursor() {
let reply = KvReply::Ok(KvOutcome::Page(KvPage {
entries: vec![KvEntry {
key: b"a".to_vec(),
value: b"1".to_vec(),
expires_at_micros: None,
version: 0,
}],
cursor: Some(b"a".to_vec()),
}));
let bytes = encode_named(&reply).expect("serializes");
let back: KvReply = decode_named(&bytes).expect("deserializes");
let KvReply::Ok(KvOutcome::Page(page)) = back else {
panic!("expected an Ok(Page) reply");
};
assert_eq!(page.entries.len(), 1);
assert_eq!(page.entries[0].key_str(), Some("a"));
assert_eq!(page.cursor.as_deref(), Some(b"a".as_ref()));
}
#[test]
fn given_a_cas_request_when_round_tripped_then_should_preserve_the_precondition() {
for expect in [CasExpect::Match(7), CasExpect::Absent] {
let request = KvCas {
v: KV_OP_VERSION,
namespace: "counters".to_owned(),
key: b"hits".to_vec(),
value: b"42".to_vec(),
expires_at_micros: None,
expect,
};
let bytes = encode_named(&request).expect("serializes");
let back: KvCas = decode_named(&bytes).expect("deserializes");
assert_eq!(back.expect, expect);
assert_eq!(back.key, b"hits");
}
}
#[test]
fn given_a_committed_reply_when_round_tripped_then_should_preserve_the_version() {
let reply = KvReply::Ok(KvOutcome::Committed { version: 9 });
let bytes = encode_named(&reply).expect("serializes");
let back: KvReply = decode_named(&bytes).expect("deserializes");
match back {
KvReply::Ok(KvOutcome::Committed { version }) => assert_eq!(version, 9),
other => panic!("expected Committed, got {other:?}"),
}
}
#[test]
fn given_a_version_conflict_when_round_tripped_then_should_preserve_the_current_version() {
for current in [Some(3u64), None] {
let reply = KvReply::Err(KvError::VersionConflict { current });
let bytes = encode_named(&reply).expect("serializes");
let back: KvReply = decode_named(&bytes).expect("deserializes");
match back {
KvReply::Err(KvError::VersionConflict { current: got }) => assert_eq!(got, current),
other => panic!("expected VersionConflict, got {other:?}"),
}
}
}
#[test]
fn given_a_versioned_entry_when_round_tripped_then_should_preserve_version_and_skip_zero() {
let entry = KvEntry {
key: b"k".to_vec(),
value: b"v".to_vec(),
expires_at_micros: None,
version: 5,
};
let bytes = encode_named(&entry).expect("serializes");
let back: KvEntry = decode_named(&bytes).expect("deserializes");
assert_eq!(back.version, 5);
let unversioned = KvEntry {
version: 0,
..entry
};
let json = serde_json::to_string(&unversioned).expect("json");
assert!(
!json.contains("version"),
"version 0 must be omitted: {json}"
);
}
#[test]
fn given_a_scan_with_bounds_when_round_tripped_then_should_preserve_filters() {
let scan = KvScan {
v: KV_OP_VERSION,
namespace: "sessions".to_owned(),
prefix: Some(b"user:".to_vec()),
start: None,
end: None,
key_contains: Some("admin".to_owned()),
limit: 50,
cursor: Some(b"user:9".to_vec()),
};
let bytes = encode_named(&scan).expect("serializes");
let back: KvScan = decode_named(&bytes).expect("deserializes");
assert_eq!(back.prefix.as_deref(), Some(b"user:".as_ref()));
assert_eq!(back.key_contains.as_deref(), Some("admin"));
assert_eq!(back.cursor.as_deref(), Some(b"user:9".as_ref()));
}
}