use anyhow::Result;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::redis::RedisPool;
pub mod keys {
pub const ORG: &str = "cache:org:";
pub const USER: &str = "cache:user:";
pub const ORG_SETTING: &str = "cache:org_setting:";
pub const USER_SETTING: &str = "cache:user_setting:";
pub const SUBSCRIPTION: &str = "cache:subscription:";
pub const PLUGIN: &str = "cache:plugin:";
pub const TEMPLATE: &str = "cache:template:";
pub const SCENARIO: &str = "cache:scenario:";
pub const ORG_MEMBERS: &str = "cache:org_members:";
}
pub mod ttl {
pub const SHORT: u64 = 60;
pub const MEDIUM: u64 = 300;
pub const LONG: u64 = 900;
pub const VERY_LONG: u64 = 3600;
pub const ORG: u64 = MEDIUM;
pub const USER: u64 = MEDIUM;
pub const SETTINGS: u64 = LONG;
pub const SUBSCRIPTION: u64 = MEDIUM;
pub const MARKETPLACE: u64 = LONG;
pub const ORG_MEMBERS: u64 = MEDIUM;
}
pub struct Cache {
redis: RedisPool,
}
impl Cache {
pub fn new(redis: RedisPool) -> Self {
Self { redis }
}
pub async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de>,
{
match self.redis.get(key).await? {
Some(value) => {
let deserialized: T = serde_json::from_str(&value)?;
Ok(Some(deserialized))
}
None => Ok(None),
}
}
pub async fn set<T>(&self, key: &str, value: &T, ttl: u64) -> Result<()>
where
T: Serialize,
{
let serialized = serde_json::to_string(value)?;
self.redis.set_with_expiry(key, &serialized, ttl).await?;
Ok(())
}
pub async fn delete(&self, key: &str) -> Result<()> {
self.redis.delete(key).await?;
Ok(())
}
pub async fn delete_pattern(&self, pattern: &str) -> Result<()> {
let keys = self.redis.scan_keys(pattern).await?;
for key in keys {
if let Err(e) = self.redis.delete(&key).await {
tracing::warn!("Failed to delete cache key {}: {}", key, e);
}
}
Ok(())
}
pub async fn invalidate_org(&self, org_id: &Uuid) -> Result<()> {
if let Err(e) = self.delete(&format!("{}:{}", keys::ORG, org_id)).await {
tracing::warn!("Failed to invalidate org cache for {}: {}", org_id, e);
}
if let Err(e) = self.delete(&format!("{}:{}", keys::ORG_MEMBERS, org_id)).await {
tracing::warn!("Failed to invalidate org members cache for {}: {}", org_id, e);
}
if let Err(e) = self.delete_pattern(&format!("{}:{}:*", keys::ORG_SETTING, org_id)).await {
tracing::warn!("Failed to invalidate org settings cache for {}: {}", org_id, e);
}
Ok(())
}
pub async fn invalidate_user(&self, user_id: &Uuid) -> Result<()> {
if let Err(e) = self.delete(&format!("{}:{}", keys::USER, user_id)).await {
tracing::warn!("Failed to invalidate user cache for {}: {}", user_id, e);
}
if let Err(e) = self.delete_pattern(&format!("{}:{}:*", keys::USER_SETTING, user_id)).await
{
tracing::warn!("Failed to invalidate user settings cache for {}: {}", user_id, e);
}
Ok(())
}
pub async fn invalidate_subscription(&self, org_id: &Uuid) -> Result<()> {
let key = format!("{}:{}", keys::SUBSCRIPTION, org_id);
self.delete(&key).await?;
Ok(())
}
pub async fn invalidate_marketplace(
&self,
content_type: &str,
content_id: &Uuid,
) -> Result<()> {
let key = match content_type {
"plugin" => format!("{}:{}", keys::PLUGIN, content_id),
"template" => format!("{}:{}", keys::TEMPLATE, content_id),
"scenario" => format!("{}:{}", keys::SCENARIO, content_id),
_ => return Ok(()),
};
self.delete(&key).await?;
Ok(())
}
pub async fn get_or_set<F, Fut, T>(&self, key: &str, ttl: u64, f: F) -> Result<T>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T>>,
T: Serialize + for<'de> Deserialize<'de>,
{
if let Some(cached) = self.get::<T>(key).await? {
return Ok(cached);
}
let value = f().await?;
if let Err(e) = self.set(key, &value, ttl).await {
tracing::warn!("Failed to cache value for key {}: {}", key, e);
}
Ok(value)
}
}
pub fn org_cache_key(org_id: &Uuid) -> String {
format!("{}:{}", keys::ORG, org_id)
}
pub fn user_cache_key(user_id: &Uuid) -> String {
format!("{}:{}", keys::USER, user_id)
}
pub fn org_setting_cache_key(org_id: &Uuid, setting_key: &str) -> String {
format!("{}:{}:{}", keys::ORG_SETTING, org_id, setting_key)
}
pub fn user_setting_cache_key(user_id: &Uuid, setting_key: &str) -> String {
format!("{}:{}:{}", keys::USER_SETTING, user_id, setting_key)
}
pub fn subscription_cache_key(org_id: &Uuid) -> String {
format!("{}:{}", keys::SUBSCRIPTION, org_id)
}
pub fn org_members_cache_key(org_id: &Uuid) -> String {
format!("{}:{}", keys::ORG_MEMBERS, org_id)
}
pub fn plugin_cache_key(plugin_id: &Uuid) -> String {
format!("{}:{}", keys::PLUGIN, plugin_id)
}
pub fn template_cache_key(template_id: &Uuid) -> String {
format!("{}:{}", keys::TEMPLATE, template_id)
}
pub fn scenario_cache_key(scenario_id: &Uuid) -> String {
format!("{}:{}", keys::SCENARIO, scenario_id)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_constants() {
assert_eq!(keys::ORG, "cache:org:");
assert_eq!(keys::USER, "cache:user:");
assert_eq!(keys::ORG_SETTING, "cache:org_setting:");
assert_eq!(keys::USER_SETTING, "cache:user_setting:");
assert_eq!(keys::SUBSCRIPTION, "cache:subscription:");
assert_eq!(keys::PLUGIN, "cache:plugin:");
assert_eq!(keys::TEMPLATE, "cache:template:");
assert_eq!(keys::SCENARIO, "cache:scenario:");
assert_eq!(keys::ORG_MEMBERS, "cache:org_members:");
}
#[test]
fn test_ttl_short() {
assert_eq!(ttl::SHORT, 60);
}
#[test]
fn test_ttl_medium() {
assert_eq!(ttl::MEDIUM, 300);
}
#[test]
fn test_ttl_long() {
assert_eq!(ttl::LONG, 900);
}
#[test]
fn test_ttl_very_long() {
assert_eq!(ttl::VERY_LONG, 3600);
}
#[test]
fn test_ttl_org() {
assert_eq!(ttl::ORG, ttl::MEDIUM);
}
#[test]
fn test_ttl_user() {
assert_eq!(ttl::USER, ttl::MEDIUM);
}
#[test]
fn test_ttl_settings() {
assert_eq!(ttl::SETTINGS, ttl::LONG);
}
#[test]
fn test_ttl_subscription() {
assert_eq!(ttl::SUBSCRIPTION, ttl::MEDIUM);
}
#[test]
fn test_ttl_marketplace() {
assert_eq!(ttl::MARKETPLACE, ttl::LONG);
}
#[test]
fn test_ttl_org_members() {
assert_eq!(ttl::ORG_MEMBERS, ttl::MEDIUM);
}
#[test]
fn test_org_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let key = org_cache_key(&id);
assert_eq!(key, "cache:org::550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_user_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440001").unwrap();
let key = user_cache_key(&id);
assert_eq!(key, "cache:user::550e8400-e29b-41d4-a716-446655440001");
}
#[test]
fn test_org_setting_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440002").unwrap();
let key = org_setting_cache_key(&id, "theme");
assert_eq!(key, "cache:org_setting::550e8400-e29b-41d4-a716-446655440002:theme");
}
#[test]
fn test_user_setting_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440003").unwrap();
let key = user_setting_cache_key(&id, "notifications");
assert_eq!(key, "cache:user_setting::550e8400-e29b-41d4-a716-446655440003:notifications");
}
#[test]
fn test_subscription_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440004").unwrap();
let key = subscription_cache_key(&id);
assert_eq!(key, "cache:subscription::550e8400-e29b-41d4-a716-446655440004");
}
#[test]
fn test_org_members_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440005").unwrap();
let key = org_members_cache_key(&id);
assert_eq!(key, "cache:org_members::550e8400-e29b-41d4-a716-446655440005");
}
#[test]
fn test_plugin_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440006").unwrap();
let key = plugin_cache_key(&id);
assert_eq!(key, "cache:plugin::550e8400-e29b-41d4-a716-446655440006");
}
#[test]
fn test_template_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440007").unwrap();
let key = template_cache_key(&id);
assert_eq!(key, "cache:template::550e8400-e29b-41d4-a716-446655440007");
}
#[test]
fn test_scenario_cache_key() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440008").unwrap();
let key = scenario_cache_key(&id);
assert_eq!(key, "cache:scenario::550e8400-e29b-41d4-a716-446655440008");
}
#[test]
fn test_cache_keys_are_unique() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let org_key = org_cache_key(&id);
let user_key = user_cache_key(&id);
let plugin_key = plugin_cache_key(&id);
let template_key = template_cache_key(&id);
let scenario_key = scenario_cache_key(&id);
let subscription_key = subscription_cache_key(&id);
assert_ne!(org_key, user_key);
assert_ne!(org_key, plugin_key);
assert_ne!(plugin_key, template_key);
assert_ne!(template_key, scenario_key);
assert_ne!(subscription_key, org_key);
}
#[test]
fn test_setting_keys_with_different_settings() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let theme_key = org_setting_cache_key(&id, "theme");
let lang_key = org_setting_cache_key(&id, "language");
assert_ne!(theme_key, lang_key);
assert!(theme_key.contains("theme"));
assert!(lang_key.contains("language"));
}
}