use platform_core::AppError;
use redis_connection::RedisBackend;
pub struct RedisCacheStore {
backend: RedisBackend,
key_prefix: String,
default_ttl_seconds: u64,
}
impl RedisCacheStore {
pub fn new(
backend: RedisBackend,
key_prefix: impl Into<String>,
default_ttl_seconds: u64,
) -> Self {
RedisCacheStore {
backend,
key_prefix: key_prefix.into(),
default_ttl_seconds,
}
}
pub fn default_ttl_seconds(&self) -> u64 {
self.default_ttl_seconds
}
pub fn backend(&self) -> &RedisBackend {
&self.backend
}
pub async fn put(
&self,
key: Option<&str>,
value: &[u8],
ttl_seconds: u64,
) -> Result<(), AppError> {
let key = self.prefixed(key)?;
self.backend
.query_idempotent::<String>(redis::cmd("SETEX").arg(key).arg(ttl_seconds).arg(value))
.await
.map(|_| ())
}
pub async fn get(&self, key: Option<&str>) -> Result<Option<Vec<u8>>, AppError> {
let key = self.prefixed(key)?;
self.backend
.query_idempotent(redis::cmd("GET").arg(key))
.await
}
pub async fn mget(&self, keys: &[String]) -> Result<Vec<(String, Vec<u8>)>, AppError> {
if keys.is_empty() {
return Ok(Vec::new());
}
let mut cmd = redis::cmd("MGET");
for key in keys {
cmd.arg(self.prefixed(Some(key))?);
}
let values: Vec<Option<Vec<u8>>> = self.backend.query_idempotent(&cmd).await?;
Ok(keys
.iter()
.zip(values)
.filter_map(|(key, value)| value.map(|bytes| (key.clone(), bytes)))
.collect())
}
pub async fn mput(
&self,
entries: &[(String, Vec<u8>)],
ttl_seconds: u64,
) -> Result<(), AppError> {
if entries.is_empty() {
return Ok(());
}
let mut pipe = redis::pipe();
for (key, value) in entries {
pipe.cmd("SETEX")
.arg(self.prefixed(Some(key))?)
.arg(ttl_seconds)
.arg(value.as_slice());
}
self.backend
.query_pipeline_idempotent::<Vec<String>>(&pipe)
.await
.map(|_| ())
}
pub async fn delete(&self, key: Option<&str>) -> Result<i64, AppError> {
let key = self.prefixed(key)?;
self.backend
.query_idempotent(redis::cmd("DEL").arg(key))
.await
}
pub async fn put_if_absent(
&self,
key: Option<&str>,
value: &[u8],
ttl_seconds: u64,
) -> Result<bool, AppError> {
let key = self.prefixed(key)?;
let reply: Option<String> = self
.backend
.query(
redis::cmd("SET")
.arg(key)
.arg(value)
.arg("NX")
.arg("EX")
.arg(ttl_seconds),
)
.await?;
Ok(reply.as_deref() == Some("OK"))
}
pub async fn list_push(
&self,
key: Option<&str>,
value: &[u8],
ttl_seconds: u64,
) -> Result<i64, AppError> {
let key = self.prefixed(key)?;
let (length, _expire_set): (i64, i64) = self
.backend
.query_pipeline(
redis::pipe()
.atomic()
.cmd("RPUSH")
.arg(&key)
.arg(value)
.cmd("EXPIRE")
.arg(&key)
.arg(ttl_seconds),
)
.await?;
Ok(length)
}
pub async fn list_pop(&self, key: Option<&str>) -> Result<Option<Vec<u8>>, AppError> {
let key = self.prefixed(key)?;
self.backend.query(redis::cmd("LPOP").arg(key)).await
}
pub async fn list_len(&self, key: Option<&str>) -> Result<i64, AppError> {
let key = self.prefixed(key)?;
self.backend
.query_idempotent(redis::cmd("LLEN").arg(key))
.await
}
fn prefixed(&self, key: Option<&str>) -> Result<String, AppError> {
match key.map(str::trim) {
Some(key) if !key.is_empty() => Ok(format!("{}{key}", self.key_prefix)),
_ => Err(AppError::new(400, "Missing 'key'")),
}
}
}