use crate::proto::pb::{CHASH_KEYFUN_BUCKETONLY, CHASH_KEYFUN_CUSTOM, CHASH_KEYFUN_STD};
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum KeyFun {
#[default]
Std,
BucketOnly,
Custom(String),
}
#[derive(Debug, thiserror::Error, Eq, PartialEq)]
#[non_exhaustive]
pub enum KeyFunError {
#[error("chash_keyfun: CUSTOM keyfun {0:?} must be routed through the WASM keyfun store")]
Custom(String),
#[error("chash_keyfun: CUSTOM keyfun module {0:?} is not registered")]
ModuleNotFound(String),
#[error("chash_keyfun: CUSTOM keyfun module {module:?} failed: {message}")]
Runtime {
module: String,
message: String,
},
#[error("chash_keyfun: CUSTOM keyfun module {0:?} exceeded its memory limit")]
MemoryLimit(String),
#[error("chash_keyfun: unknown selector {0}")]
Unknown(u32),
}
impl KeyFun {
#[must_use]
pub fn route_bytes(&self, bucket: &[u8], key: &[u8]) -> Vec<u8> {
self.try_route_bytes(bucket, key)
.expect("invariant: route_bytes called on KeyFun::Custom; route through BucketRouter")
}
pub fn try_route_bytes(&self, bucket: &[u8], key: &[u8]) -> Result<Vec<u8>, KeyFunError> {
match self {
Self::Std => {
let mut out = Vec::with_capacity(bucket.len() + 1 + key.len());
out.extend_from_slice(bucket);
out.push(b'/');
out.extend_from_slice(key);
Ok(out)
}
Self::BucketOnly => Ok(bucket.to_vec()),
Self::Custom(id) => Err(KeyFunError::Custom(id.clone())),
}
}
pub fn route_bytes_into(&self, bucket: &[u8], key: &[u8], buf: &mut Vec<u8>) {
match self {
Self::Std => {
buf.extend_from_slice(bucket);
buf.push(b'/');
buf.extend_from_slice(key);
}
Self::BucketOnly => {
buf.extend_from_slice(bucket);
}
Self::Custom(_) => {
panic!(
"invariant: route_bytes_into called on KeyFun::Custom; route through BucketRouter"
);
}
}
}
#[must_use]
pub fn is_custom(&self) -> bool {
matches!(self, Self::Custom(_))
}
#[must_use]
pub fn custom_module(&self) -> Option<&str> {
match self {
Self::Custom(id) => Some(id.as_str()),
Self::Std | Self::BucketOnly => None,
}
}
#[must_use]
pub fn route_len(&self, bucket: &[u8], key: &[u8]) -> Option<usize> {
match self {
Self::Std => Some(bucket.len() + 1 + key.len()),
Self::BucketOnly => Some(bucket.len()),
Self::Custom(_) => None,
}
}
pub fn from_wire(value: u32) -> Result<Self, KeyFunError> {
match value {
CHASH_KEYFUN_STD => Ok(Self::Std),
CHASH_KEYFUN_BUCKETONLY => Ok(Self::BucketOnly),
CHASH_KEYFUN_CUSTOM => Ok(Self::Custom(String::new())),
other => Err(KeyFunError::Unknown(other)),
}
}
#[must_use]
pub fn to_wire(&self) -> u32 {
match self {
Self::Std => CHASH_KEYFUN_STD,
Self::BucketOnly => CHASH_KEYFUN_BUCKETONLY,
Self::Custom(_) => CHASH_KEYFUN_CUSTOM,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn std_shapes_bucket_slash_key() {
let bytes = KeyFun::Std.route_bytes(b"users", b"alice");
assert_eq!(bytes, b"users/alice");
}
#[test]
fn bucket_only_drops_key() {
let a = KeyFun::BucketOnly.route_bytes(b"users", b"alice");
let b = KeyFun::BucketOnly.route_bytes(b"users", b"bob");
assert_eq!(a, b);
assert_eq!(a, b"users");
}
#[test]
fn route_bytes_into_matches_route_bytes() {
for kf in [KeyFun::Std, KeyFun::BucketOnly] {
let owned = kf.route_bytes(b"b", b"k");
let mut buf = Vec::new();
kf.route_bytes_into(b"b", b"k", &mut buf);
assert_eq!(owned, buf, "kf = {kf:?}");
assert_eq!(owned.len(), kf.route_len(b"b", b"k").unwrap());
}
}
#[test]
fn custom_try_route_bytes_is_error() {
let kf = KeyFun::Custom("rev".to_string());
assert_eq!(
kf.try_route_bytes(b"b", b"k"),
Err(KeyFunError::Custom("rev".to_string()))
);
assert!(kf.is_custom());
assert_eq!(kf.custom_module(), Some("rev"));
assert_eq!(kf.route_len(b"b", b"k"), None);
}
#[test]
#[should_panic(expected = "KeyFun::Custom")]
fn custom_route_bytes_panics() {
let _ = KeyFun::Custom("rev".to_string()).route_bytes(b"b", b"k");
}
#[test]
fn from_wire_round_trips() {
for kf in [KeyFun::Std, KeyFun::BucketOnly] {
let w = kf.to_wire();
let back = KeyFun::from_wire(w).expect("known");
assert_eq!(back, kf);
}
}
#[test]
fn from_wire_custom_yields_empty_module_id() {
assert_eq!(KeyFun::from_wire(99), Ok(KeyFun::Custom(String::new())));
assert_eq!(KeyFun::Custom("anything".to_string()).to_wire(), 99);
}
#[test]
fn from_wire_rejects_unknown() {
assert_eq!(KeyFun::from_wire(7), Err(KeyFunError::Unknown(7)));
}
#[test]
fn default_is_std() {
assert_eq!(KeyFun::default(), KeyFun::Std);
}
#[test]
fn empty_inputs_are_total() {
assert_eq!(KeyFun::Std.route_bytes(b"", b""), b"/");
assert_eq!(KeyFun::BucketOnly.route_bytes(b"", b""), b"");
}
}