use std::marker::PhantomData;
use std::sync::Arc;
use std::time::Duration;
use serde::Serialize;
use serde::de::DeserializeOwned;
use rskit_errors::{AppError, AppResult, ErrorCode};
use crate::registry::CacheStore;
pub struct TypedStore<T> {
client: Arc<dyn CacheStore>,
prefix: String,
_marker: PhantomData<T>,
}
impl<T: Serialize + DeserializeOwned + Send + Sync> TypedStore<T> {
pub fn new(client: Arc<dyn CacheStore>, prefix: impl Into<String>) -> Self {
Self {
client,
prefix: prefix.into(),
_marker: PhantomData,
}
}
fn full_key(&self, key: &str) -> String {
format!("{}:{}", self.prefix, key)
}
pub async fn get(&self, key: &str) -> AppResult<Option<T>> {
let raw = self.client.get(&self.full_key(key)).await?;
match raw {
Some(json) => {
let val = serde_json::from_str(&json).map_err(|e| {
AppError::new(ErrorCode::Internal, format!("json deserialise error: {e}"))
.with_cause(e)
})?;
Ok(Some(val))
}
None => Ok(None),
}
}
pub async fn set(&self, key: &str, val: &T, ttl: Option<Duration>) -> AppResult<()> {
let json = serde_json::to_string(val).map_err(|e| {
AppError::new(ErrorCode::Internal, format!("json serialise error: {e}")).with_cause(e)
})?;
self.client.set(&self.full_key(key), &json, ttl).await
}
pub async fn delete(&self, key: &str) -> AppResult<bool> {
self.client.delete(&self.full_key(key)).await
}
pub async fn exists(&self, key: &str) -> AppResult<bool> {
self.client.exists(&self.full_key(key)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use serde::Serializer;
use std::collections::BTreeMap;
#[derive(Default)]
struct MemoryStore {
values: Mutex<BTreeMap<String, String>>,
}
#[async_trait::async_trait]
impl CacheStore for MemoryStore {
async fn get(&self, key: &str) -> AppResult<Option<String>> {
Ok(self.values.lock().get(key).cloned())
}
async fn set(&self, key: &str, val: &str, _ttl: Option<Duration>) -> AppResult<()> {
self.values.lock().insert(key.to_string(), val.to_string());
Ok(())
}
async fn delete(&self, key: &str) -> AppResult<bool> {
Ok(self.values.lock().remove(key).is_some())
}
async fn exists(&self, key: &str) -> AppResult<bool> {
Ok(self.values.lock().contains_key(key))
}
}
struct FailingSerialize;
impl Serialize for FailingSerialize {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Err(serde::ser::Error::custom("boom"))
}
}
impl<'de> serde::Deserialize<'de> for FailingSerialize {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
serde::de::IgnoredAny::deserialize(deserializer)?;
Ok(Self)
}
}
#[tokio::test]
async fn typed_store_round_trips_prefixed_json() {
let store = Arc::new(MemoryStore::default());
let typed = TypedStore::<u32>::new(store.clone(), "numbers");
typed
.set("answer", &42, None)
.await
.expect("set should serialise");
assert!(store.exists("numbers:answer").await.expect("exists works"));
assert_eq!(
typed.get("answer").await.expect("get should deserialise"),
Some(42)
);
assert!(typed.delete("answer").await.expect("delete should succeed"));
assert_eq!(
typed.get("answer").await.expect("missing key succeeds"),
None
);
}
#[tokio::test]
async fn get_rejects_invalid_json() {
let store = Arc::new(MemoryStore::default());
store
.set("numbers:bad", "not-json", None)
.await
.expect("fixture write succeeds");
let typed = TypedStore::<u32>::new(store, "numbers");
let err = typed
.get("bad")
.await
.expect_err("invalid json should fail");
assert_eq!(err.code(), ErrorCode::Internal);
}
#[tokio::test]
async fn set_rejects_serialisation_errors() {
let store = Arc::new(MemoryStore::default());
let typed = TypedStore::<FailingSerialize>::new(store.clone(), "bad");
let err = typed
.set("value", &FailingSerialize, None)
.await
.expect_err("serialisation errors should surface");
assert_eq!(err.code(), ErrorCode::Internal);
store
.set("bad:value", "null", None)
.await
.expect("fixture write succeeds");
assert!(
typed
.get("value")
.await
.expect("deserialise should succeed")
.is_some()
);
}
}