use async_trait::async_trait;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("storage backend error: {0}")]
Backend(String),
}
#[async_trait]
pub trait StorageBackend: Send + Sync + std::fmt::Debug {
async fn save(
&self,
namespace: &str,
key: &str,
value: serde_json::Value,
) -> Result<(), StorageError>;
async fn get(
&self,
namespace: &str,
key: &str,
) -> Result<Option<serde_json::Value>, StorageError>;
async fn list(
&self,
namespace: &str,
prefix: &str,
) -> Result<Vec<(String, serde_json::Value)>, StorageError>;
async fn delete(&self, namespace: &str, key: &str) -> Result<(), StorageError>;
}
#[derive(Debug, Default)]
pub struct InMemoryStorageBackend {
inner: RwLock<HashMap<String, HashMap<String, serde_json::Value>>>,
}
impl InMemoryStorageBackend {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl StorageBackend for InMemoryStorageBackend {
async fn save(
&self,
namespace: &str,
key: &str,
value: serde_json::Value,
) -> Result<(), StorageError> {
let mut guard = self.inner.write();
guard
.entry(namespace.to_string())
.or_default()
.insert(key.to_string(), value);
Ok(())
}
async fn get(
&self,
namespace: &str,
key: &str,
) -> Result<Option<serde_json::Value>, StorageError> {
let guard = self.inner.read();
Ok(guard.get(namespace).and_then(|ns| ns.get(key)).cloned())
}
async fn list(
&self,
namespace: &str,
prefix: &str,
) -> Result<Vec<(String, serde_json::Value)>, StorageError> {
let guard = self.inner.read();
let Some(ns) = guard.get(namespace) else {
return Ok(Vec::new());
};
Ok(ns
.iter()
.filter(|(k, _)| k.starts_with(prefix))
.map(|(k, v)| (k.clone(), v.clone()))
.collect())
}
async fn delete(&self, namespace: &str, key: &str) -> Result<(), StorageError> {
let mut guard = self.inner.write();
if let Some(ns) = guard.get_mut(namespace) {
ns.remove(key);
}
Ok(())
}
}
#[must_use]
pub fn default_storage_backend() -> Arc<dyn StorageBackend> {
Arc::new(InMemoryStorageBackend::new())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn save_get_roundtrip() {
let b = InMemoryStorageBackend::new();
b.save("ns", "k", json!({"x": 1})).await.unwrap();
assert_eq!(b.get("ns", "k").await.unwrap(), Some(json!({"x": 1})));
}
#[tokio::test]
async fn delete_is_idempotent_for_missing_keys() {
let b = InMemoryStorageBackend::new();
b.delete("ns", "absent").await.unwrap();
b.save("ns", "k", json!(1)).await.unwrap();
b.delete("ns", "absent").await.unwrap();
assert_eq!(b.get("ns", "k").await.unwrap(), Some(json!(1)));
}
#[tokio::test]
async fn list_returns_only_prefix_matches() {
let b = InMemoryStorageBackend::new();
b.save("ns", "user/1", json!(1)).await.unwrap();
b.save("ns", "post/1", json!(2)).await.unwrap();
let got = b.list("ns", "user/").await.unwrap();
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, "user/1");
}
#[tokio::test]
async fn namespaces_are_isolated() {
let b = InMemoryStorageBackend::new();
b.save("a", "k", json!(1)).await.unwrap();
b.save("b", "k", json!(2)).await.unwrap();
assert_eq!(b.get("a", "k").await.unwrap(), Some(json!(1)));
assert_eq!(b.get("b", "k").await.unwrap(), Some(json!(2)));
}
}