use crate::backend::CacheBackend;
use crate::error::OxCacheResult;
use crate::traits::CacheKey;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
pub trait NamespaceName: Send + Sync + 'static {
const NAME: &'static str;
}
#[macro_export]
macro_rules! namespace {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct $name;
impl $crate::cache::typed_namespace::NamespaceName for $name {
const NAME: &'static str = stringify!($name);
}
};
}
pub struct TypedNamespace<K, V, N: NamespaceName> {
backend: Arc<dyn CacheBackend>,
prefix: String,
_phantom: PhantomData<(K, V, N)>,
}
impl<K, V, N> std::fmt::Debug for TypedNamespace<K, V, N>
where
K: CacheKey,
V: serde::Serialize + for<'de> serde::Deserialize<'de>,
N: NamespaceName,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TypedNamespace")
.field("namespace", &<N as NamespaceName>::NAME)
.field("prefix", &self.prefix)
.finish()
}
}
impl<K, V, N> TypedNamespace<K, V, N>
where
K: CacheKey,
V: serde::Serialize + for<'de> serde::Deserialize<'de>,
N: NamespaceName,
{
pub fn scoped(backend: Arc<dyn CacheBackend>) -> Self {
Self {
backend,
prefix: format!("{}:", <N as NamespaceName>::NAME),
_phantom: PhantomData,
}
}
pub fn full_key(&self, key: &K) -> String {
format!("{}{}", self.prefix, key.to_key_string())
}
pub fn namespace(&self) -> &'static str {
<N as NamespaceName>::NAME
}
pub async fn get(&self, key: &K) -> OxCacheResult<Option<V>> {
let full = self.full_key(key);
match self.backend.get(&full).await? {
Some(data) => {
let val: V = crate::infra::serialization::depth_limited::deserialize_safe(
&data,
crate::core::constants::MAX_JSON_DEPTH,
)
.map_err(|e| crate::error::OxCacheError::Serialization(e.to_string()))?;
Ok(Some(val))
}
None => Ok(None),
}
}
pub async fn set(&self, key: &K, value: &V, ttl: Option<Duration>) -> OxCacheResult<()> {
let full = self.full_key(key);
let bytes = serde_json::to_vec(value)
.map_err(|e| crate::error::OxCacheError::Serialization(e.to_string()))?;
self.backend
.set(Arc::from(full), Arc::new(bytes), ttl)
.await
}
pub async fn delete(&self, key: &K) -> OxCacheResult<()> {
self.backend.delete(&self.full_key(key)).await
}
pub async fn exists(&self, key: &K) -> OxCacheResult<bool> {
self.backend.exists(&self.full_key(key)).await
}
pub async fn len(&self) -> OxCacheResult<u64> {
let keys = self.backend.keys(&format!("{}*", self.prefix)).await?;
Ok(keys.len() as u64)
}
pub async fn is_empty(&self) -> OxCacheResult<bool> {
Ok(self.len().await? == 0)
}
pub async fn invalidate_all(&self) -> OxCacheResult<u64> {
let pattern = format!("{}*", self.prefix);
let keys = self.backend.keys(&pattern).await?;
let count = keys.len() as u64;
for key in keys {
self.backend.delete(&key).await?;
}
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::MockBackend;
use serde::{Deserialize, Serialize};
namespace!(Users);
namespace!(Orders);
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
struct User {
id: u64,
name: String,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
struct Order {
id: u64,
total: f64,
}
fn backend() -> Arc<dyn CacheBackend> {
Arc::new(MockBackend::new("mock", 100, false))
}
#[tokio::test]
async fn typed_roundtrip_with_namespace_prefix() {
let ns: TypedNamespace<String, User, Users> = TypedNamespace::scoped(backend());
let user = User {
id: 1,
name: "alice".into(),
};
ns.set(&"1".to_string(), &user, None).await.unwrap();
assert_eq!(ns.get(&"1".to_string()).await.unwrap(), Some(user));
assert!(ns.exists(&"1".to_string()).await.unwrap());
assert_eq!(ns.full_key(&"1".to_string()), "Users:1");
}
#[tokio::test]
async fn namespaces_are_isolated_at_runtime() {
let backend = backend();
let users: TypedNamespace<String, User, Users> = TypedNamespace::scoped(backend.clone());
let orders: TypedNamespace<String, Order, Orders> = TypedNamespace::scoped(backend.clone());
users
.set(
&"1".to_string(),
&User {
id: 1,
name: "a".into(),
},
None,
)
.await
.unwrap();
let missing: Option<Order> = orders.get(&"1".to_string()).await.unwrap();
assert!(missing.is_none(), "跨命名空间同形键不得互相可见");
assert_eq!(users.len().await.unwrap(), 1);
assert_eq!(orders.len().await.unwrap(), 0);
assert_eq!(users.namespace(), "Users");
assert_eq!(orders.namespace(), "Orders");
}
#[tokio::test]
async fn invalidate_all_removes_only_own_prefix() {
let backend = backend();
let users: TypedNamespace<String, User, Users> = TypedNamespace::scoped(backend.clone());
let orders: TypedNamespace<String, Order, Orders> = TypedNamespace::scoped(backend.clone());
users
.set(
&"1".to_string(),
&User {
id: 1,
name: "a".into(),
},
None,
)
.await
.unwrap();
users
.set(
&"2".to_string(),
&User {
id: 2,
name: "b".into(),
},
None,
)
.await
.unwrap();
orders
.set(&"1".to_string(), &Order { id: 1, total: 9.9 }, None)
.await
.unwrap();
let removed = users.invalidate_all().await.unwrap();
assert_eq!(removed, 2, "应只清除 Users 前缀的 2 条");
assert!(users.is_empty().await.unwrap());
assert_eq!(orders.len().await.unwrap(), 1, "Orders 命名空间不受影响");
assert!(!backend.exists("Users:1").await.unwrap());
assert!(backend.exists("Orders:1").await.unwrap());
}
#[tokio::test]
async fn delete_and_ttl_passthrough() {
let ns: TypedNamespace<String, User, Users> = TypedNamespace::scoped(backend());
ns.set(
&"k".to_string(),
&User {
id: 3,
name: "c".into(),
},
None,
)
.await
.unwrap();
ns.delete(&"k".to_string()).await.unwrap();
assert_eq!(ns.get(&"k".to_string()).await.unwrap(), None);
}
#[tokio::test]
async fn wrong_value_type_is_error_not_garbage() {
let backend = backend();
let ns: TypedNamespace<String, User, Users> = TypedNamespace::scoped(backend.clone());
backend
.set(
Arc::from("Users:bad"),
Arc::new(br#"{"id":9,"total":1.0}"#.to_vec()),
None,
)
.await
.unwrap();
assert!(ns.get(&"bad".to_string()).await.is_err());
}
}