use crate::db::get_redis_conn;
use crate::types::DynError;
use deadpool_redis::redis::Script;
use deadpool_redis::redis::{AsyncCommands, JsonAsyncCommands};
use serde::{de::DeserializeOwned, Serialize};
use tracing::{debug, trace};
#[derive(Clone, Debug)]
pub enum JsonAction {
Increment(i64),
Decrement(i64),
}
pub struct ValueRange {
min: i64,
max: i64,
}
impl Default for ValueRange {
fn default() -> Self {
Self {
min: 0,
max: u32::MAX as i64,
}
}
}
pub async fn put<T: Serialize + Send + Sync>(
prefix: &str,
key: &str,
value: &T,
path: Option<&str>,
expiration: Option<i64>,
) -> Result<(), DynError> {
let index_key = format!("{prefix}:{key}");
match serde_json::to_value(value)? {
serde_json::Value::Bool(boolean_value) => {
handle_put_boolean(&index_key, boolean_value, expiration).await?;
}
_ => {
handle_put_json(&index_key, value, path, expiration).await?;
}
}
debug!(
"Set key: {} with optional expiration: {:?}",
index_key, expiration
);
Ok(())
}
pub async fn modify_json_field(
prefix: &str,
key: &str,
field: &str,
action: JsonAction,
range: Option<ValueRange>,
) -> Result<(), DynError> {
let mut redis_conn = get_redis_conn().await?;
let index_key = format!("{prefix}:{key}");
let json_path = format!("$.{field}");
let amount = match action {
JsonAction::Increment(value) => value,
JsonAction::Decrement(value) => -value, };
let range = range.unwrap_or_default();
let script = Script::new(
r#"
local path = ARGV[1]
local amount = tonumber(ARGV[2])
local min_value = tonumber(ARGV[3])
local max_value = tonumber(ARGV[4])
local current = 0
-- Fetch the current value as a JSON string
local current_value = redis.call('JSON.GET', KEYS[1], path)
if current_value ~= nil then
-- Decode the JSON string into a Lua table
local decoded = cjson.decode(current_value)
if type(decoded) == 'table' then
-- If the decoded value is an array, extract the first element
if #decoded > 0 then
current = tonumber(decoded[1]) or 0
end
elseif type(decoded) == 'number' then
-- If the decoded value is a number, use it directly
current = decoded
end
end
local new_value = current + amount
-- Enforce min and max boundaries
if new_value < min_value then
new_value = min_value
elseif new_value > max_value then
new_value = max_value
end
-- Set the new value
redis.call('JSON.SET', KEYS[1], path, new_value)
return new_value
"#,
);
debug!(
"Modifiying field: {} in key: {} by {}",
field, index_key, amount
);
let _: i64 = script
.key(index_key)
.arg(json_path)
.arg(amount.to_string())
.arg(range.min.to_string())
.arg(range.max.to_string())
.invoke_async(&mut redis_conn)
.await?;
Ok(())
}
async fn handle_put_boolean(
key: &str,
value: bool,
expiration: Option<i64>,
) -> Result<(), DynError> {
let mut redis_conn = get_redis_conn().await?;
let int_value = if value { 1 } else { 0 };
if let Some(exp) = expiration {
let _: () = redis_conn.set_ex(key, int_value, exp as u64).await?;
} else {
let _: () = redis_conn.set(key, int_value).await?;
}
Ok(())
}
async fn handle_put_json<T: Serialize + Send + Sync>(
key: &str,
value: &T,
path: Option<&str>,
expiration: Option<i64>,
) -> Result<(), DynError> {
let mut redis_conn = get_redis_conn().await?;
let json_path = path.unwrap_or("$");
let _: () = redis_conn.json_set(key, json_path, value).await?;
if let Some(exp) = expiration {
let _: () = redis_conn.expire(key, exp).await?;
}
Ok(())
}
pub async fn put_multiple<T: Serialize>(
prefix: &str,
data: &[(impl AsRef<str>, T)],
) -> Result<(), DynError> {
let mut redis_conn = get_redis_conn().await?;
let mut cmd = redis::pipe();
for (key, value) in data {
let full_key = format!("{}:{}", prefix, key.as_ref());
match serde_json::to_value(value)? {
serde_json::Value::Bool(boolean_value) => {
let int_value = if boolean_value { 1 } else { 0 };
cmd.set(&full_key, int_value);
}
_ => {
cmd.json_set(&full_key, "$", value)?;
}
}
}
let _: () = cmd.query_async(&mut redis_conn).await?;
Ok(())
}
pub async fn get<T: DeserializeOwned + Send + Sync>(
prefix: &str,
key: &str,
path: Option<&str>,
) -> Result<Option<T>, DynError> {
let mut redis_conn = get_redis_conn().await?;
let index_key = format!("{prefix}:{key}");
let json_path = path.unwrap_or("$").to_string();
if let Ok(indexed_value) = redis_conn
.json_get::<String, String, String>(index_key.clone(), json_path)
.await
{
let value: Vec<T> = serde_json::from_str(&indexed_value)?;
return Ok(value.into_iter().next()); }
Ok(None)
}
pub async fn get_multiple<T: DeserializeOwned + Send + Sync>(
prefix: &str,
keys: &[impl AsRef<str>],
path: Option<&str>,
) -> Result<Vec<Option<T>>, DynError> {
let mut redis_conn = get_redis_conn().await?;
let json_path = path.unwrap_or("$");
let full_keys: Vec<String> = keys
.iter()
.map(|key| format!("{}:{}", prefix, key.as_ref()))
.collect();
let indexed_values: Vec<Option<String>> = redis_conn.json_get(&full_keys, json_path).await?;
let results: Vec<Option<T>> = if indexed_values.is_empty() {
(0..keys.len()).map(|_| None).collect()
} else {
deserialize_values(indexed_values)?
};
Ok(results)
}
fn deserialize_values<T: DeserializeOwned>(
values: Vec<Option<String>>,
) -> Result<Vec<Option<T>>, DynError> {
values
.into_iter()
.map(|value_str| match value_str {
Some(value) => {
let value: Vec<T> = serde_json::from_str(&value)?;
Ok(value.into_iter().next())
}
None => Ok(None),
})
.collect()
}
pub async fn _get_bool(prefix: &str, key: &str) -> Result<Option<bool>, DynError> {
let mut redis_conn = get_redis_conn().await?;
let index_key = format!("{prefix}:{key}");
if let Ok(indexed_value) = redis_conn.get::<_, i32>(&index_key).await {
trace!(
"Restored boolean key: {} with value: {}",
index_key,
indexed_value
);
let value = match indexed_value {
1 => true,
0 => false,
_ => return Ok(None), };
return Ok(Some(value));
}
Ok(None)
}
pub async fn del_multiple(prefix: &str, keys: &[impl AsRef<str>]) -> Result<(), DynError> {
let mut redis_conn = get_redis_conn().await?;
let full_keys: Vec<String> = keys
.iter()
.map(|key| format!("{}:{}", prefix, key.as_ref()))
.collect();
let _: () = redis_conn.del(full_keys).await?;
Ok(())
}