use crate::cache::contract::CacheDriverContract;
use crate::prelude::Redis;
use crate::results::AppResult;
use std::sync::Arc;
#[derive(Clone)]
pub struct RedisCacheDriver {
redis: Arc<Redis>,
}
impl RedisCacheDriver {
pub fn new(redis: Arc<Redis>) -> Self {
Self { redis }
}
}
#[async_trait::async_trait]
impl CacheDriverContract for RedisCacheDriver {
async fn keys(&self) -> AppResult<Vec<String>> {
self.redis.keys().await
}
async fn keys_by_pattern(&self, pattern: &str) -> AppResult<Vec<String>> {
let redis_pattern = regex_to_redis_pattern(pattern);
self.redis.keys_by_pattern(&redis_pattern).await
}
async fn put_raw(&self, key: &str, value: String) -> AppResult<String> {
self.redis.set(key, &value).await
}
async fn get_raw(&self, key: &str) -> AppResult<Option<String>> {
self.redis.get::<Option<String>>(key).await
}
async fn forget(&self, key: &str) -> AppResult<i32> {
self.redis.delete(key).await
}
async fn forget_by_pattern(&self, key: &str) -> AppResult<i32> {
self.redis
.delete_by_pattern(key)
.await
.map(|count| count as i32)
}
}
fn regex_to_redis_pattern(pattern: &str) -> String {
let mut redis_pattern = pattern.to_string();
redis_pattern = redis_pattern.replace("^", "");
redis_pattern = redis_pattern.replace("$", "");
redis_pattern = redis_pattern.replace(".*", "*");
redis_pattern = redis_pattern.replace(".", "?");
redis_pattern = redis_pattern.replace("(?i)", "");
redis_pattern = redis_pattern.replace("[", "\\[");
redis_pattern = redis_pattern.replace("]", "\\]");
redis_pattern
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::Redis;
use deadpool_redis::{Config, Runtime};
use std::env;
use std::sync::Arc;
use std::time::Duration;
async fn setup_test_driver() -> Option<RedisCacheDriver> {
let redis_url = env::var("TEST_REDIS_DSN")
.or_else(|_| env::var("REDIS_DSN"))
.unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
eprintln!("Connecting to Redis at {redis_url}");
let cfg = Config::from_url(redis_url);
match cfg.create_pool(Some(Runtime::Tokio1)) {
Ok(pool) => {
let redis = Arc::new(Redis::new(pool));
let driver = RedisCacheDriver::new(redis);
match driver.redis.flush_db().await {
Ok(_) => Some(driver),
Err(e) => {
eprintln!("Error: {e}");
None
}
}
}
Err(e) => {
eprintln!("Error: {e}");
None
}
}
}
#[tokio::test]
async fn test_basic_operations() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
match driver.redis.flush_db().await {
Ok(_) => eprintln!("Successfully connected to Redis and flushed DB"),
Err(e) => {
eprintln!("Failed to flush Redis DB: {e}",);
return;
}
}
let key = "_____test_key";
let value = "test_value".to_string();
assert!(driver.put_raw(key, value.clone()).await.is_ok());
let get_result = driver.get_raw(key).await.unwrap();
assert_eq!(get_result, Some(value));
assert_eq!(driver.forget(key).await.unwrap(), 1);
assert_eq!(driver.get_raw(key).await.unwrap(), None);
assert_eq!(driver.forget("nonexistent").await.unwrap(), 0);
}
#[tokio::test]
async fn test_forget_by_pattern_comprehensive() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
match driver.redis.flush_db().await {
Ok(_) => eprintln!("Successfully connected to Redis and flushed DB"),
Err(e) => {
eprintln!("Failed to flush Redis DB: {e}");
return;
}
}
let test_data = [
("user:123", "data1"),
("user:456", "data2"),
("cache:temp:1", "temp1"),
("cache:temp:2", "temp2"),
("session:abc", "session1"),
("SESSION:xyz", "session2"),
("test.key", "value"),
("test-key", "value"),
("special*char", "special"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let removed = driver.forget_by_pattern("user:*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("user:123").await.unwrap(), None);
assert_eq!(driver.get_raw("user:456").await.unwrap(), None);
assert!(driver.get_raw("cache:temp:1").await.unwrap().is_some());
let removed = driver.forget_by_pattern("cache:temp:*").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("cache:temp:1").await.unwrap(), None);
assert_eq!(driver.get_raw("cache:temp:2").await.unwrap(), None);
let removed = driver.forget_by_pattern("test?key").await.unwrap();
assert_eq!(removed, 2);
assert_eq!(driver.get_raw("test.key").await.unwrap(), None);
assert_eq!(driver.get_raw("test-key").await.unwrap(), None);
let removed = driver.forget_by_pattern("special\\*char").await.unwrap();
assert_eq!(removed, 1);
assert_eq!(driver.get_raw("special*char").await.unwrap(), None);
}
#[tokio::test]
async fn test_concurrent_operations() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
match driver.redis.flush_db().await {
Ok(_) => eprintln!("Successfully connected to Redis and flushed DB"),
Err(e) => {
eprintln!("Failed to flush Redis DB: {e}");
return;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
let driver_clone = driver.clone();
for i in 0..100 {
let key = format!("test:{i}");
let value = format!("value{i}");
match driver.put_raw(&key, value.clone()).await {
Ok(_) => {
match driver.get_raw(&key).await {
Ok(Some(v)) if v == value => continue,
Ok(Some(v)) => {
eprintln!(
"Value mismatch for key {key}: expected '{value}', got '{v}'"
);
return;
}
Ok(None) => {
eprintln!("Key {key} was written but returned None on read");
return;
}
Err(e) => {
eprintln!("Error reading back key {key}: {e}");
return;
}
}
}
Err(e) => {
eprintln!("Failed to write key {key}: {e}");
return;
}
}
}
eprintln!("Initial data written successfully");
for i in 0..100 {
let key = format!("test:{i}");
let expected = format!("value{i}");
match driver.get_raw(&key).await {
Ok(Some(v)) if v == expected => continue,
Ok(Some(v)) => {
eprintln!(
"Pre-concurrent check: Value mismatch for key {key}: expected '{expected}', got '{v}'"
);
return;
}
Ok(None) => {
eprintln!("Pre-concurrent check: Key {key} unexpectedly returned None");
return;
}
Err(e) => {
eprintln!("Pre-concurrent check: Error reading key {key}: {e}");
return;
}
}
}
eprintln!("Initial verification completed successfully");
let barrier = Arc::new(tokio::sync::Barrier::new(2));
let barrier_write = barrier.clone();
let barrier_read = barrier.clone();
let write_handle = tokio::spawn({
let driver = driver_clone.clone();
async move {
barrier_write.wait().await;
for i in 100..200 {
if let Err(e) = driver
.put_raw(&format!("test:{i}"), format!("value{i}"))
.await
{
eprintln!("Write task error: {e}");
return;
}
}
}
});
let read_handle = tokio::spawn({
let driver = driver_clone.clone();
async move {
barrier_read.wait().await;
for i in 0..100 {
match driver.get_raw(&format!("test:{i}")).await {
Ok(Some(v)) => {
let expected = format!("value{i}");
if v != expected {
eprintln!(
"Read task: Value mismatch for key test:{i}: expected '{expected}', got '{v}'"
);
}
}
Ok(None) => eprintln!("Read task: Unexpected None for key test:{i}"),
Err(e) => eprintln!("Read task error for key test:{i}: {e}"),
}
}
}
});
let _ = tokio::join!(write_handle, read_handle);
let removed = driver.forget_by_pattern("test:*").await.unwrap();
assert_eq!(removed, 200);
}
#[tokio::test]
async fn test_error_handling() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
match driver.redis.flush_db().await {
Ok(_) => eprintln!("Successfully connected to Redis and flushed DB"),
Err(e) => {
eprintln!("Failed to flush Redis DB: {e}");
return;
}
}
let special_chars = vec!["key with spaces", "key\nwith\nnewlines", "key:with:colons"];
for key in special_chars {
let result = driver.put_raw(key, "test".to_string()).await;
assert!(result.is_ok(), "Should handle special characters in keys");
}
}
#[tokio::test]
async fn test_expiration_behavior() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
match driver.redis.flush_db().await {
Ok(_) => eprintln!("Successfully connected to Redis and flushed DB"),
Err(e) => {
eprintln!("Failed to flush Redis DB: {e}");
return;
}
}
let key = "expiring_key";
let value = "test_value".to_string();
driver.put_raw(key, value).await.unwrap();
let result = driver.get_raw(key).await.unwrap();
assert!(result.is_some(), "Value should still exist after 1 second");
}
#[tokio::test]
async fn test_keys_empty_db() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let keys = driver.keys().await.unwrap();
assert!(keys.is_empty(), "Empty database should return no keys");
}
#[tokio::test]
async fn test_keys_with_data() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let test_data = [
("key1", "value1"),
("key2", "value2"),
("prefix:key3", "value3"),
("", "empty_value"), ];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys().await.unwrap();
keys.sort();
let mut expected: Vec<String> = test_data.iter().map(|(k, _)| k.to_string()).collect();
expected.sort();
assert_eq!(keys, expected, "Retrieved keys should match inserted keys");
}
#[tokio::test]
async fn test_keys_after_deletion() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let test_data = [
("test1", "value1"),
("test2", "value2"),
("test3", "value3"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
driver.forget("test2").await.unwrap();
let mut keys = driver.keys().await.unwrap();
keys.sort();
let expected = vec!["test1".to_string(), "test3".to_string()];
assert_eq!(keys, expected, "Keys should not include deleted key");
}
#[tokio::test]
async fn test_keys_by_pattern_basic() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let test_data = [
("user:1", "value1"),
("user:2", "value2"),
("profile:1", "value3"),
("other", "value4"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys_by_pattern("^user:.*").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec!["user:1".to_string(), "user:2".to_string()],
"Should match user: prefix"
);
let keys = driver.keys_by_pattern("^other$").await.unwrap();
assert_eq!(
keys,
vec!["other".to_string()],
"Should match exact pattern"
);
}
#[tokio::test]
async fn test_keys_by_pattern_wildcards() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let test_data = [
("test1", "value1"),
("test2", "value2"),
("test11", "value3"),
("atest1", "value4"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys_by_pattern("test.").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec!["test1".to_string(), "test2".to_string()],
"Should match single character wildcard"
);
let mut keys = driver.keys_by_pattern("test.*").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec![
"test1".to_string(),
"test11".to_string(),
"test2".to_string()
],
"Should match multi-character wildcard"
);
}
#[tokio::test]
async fn test_keys_by_pattern_special_chars() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
let test_data = [
("test[1]", "value1"),
("test[2]", "value2"),
("test{3}", "value3"),
];
for (key, value) in test_data {
driver.put_raw(key, value.to_string()).await.unwrap();
}
let mut keys = driver.keys_by_pattern("test\\[.*\\]").await.unwrap();
keys.sort();
assert_eq!(
keys,
vec!["test[1]".to_string(), "test[2]".to_string()],
"Should match escaped special characters"
);
}
#[tokio::test]
async fn test_keys_by_pattern_empty() {
let driver = match setup_test_driver().await {
Some(driver) => driver,
None => {
eprintln!("Skipping Redis tests - no connection available");
return;
}
};
driver.redis.flush_db().await.unwrap();
driver.put_raw("test1", "value1".to_string()).await.unwrap();
driver.put_raw("test2", "value2".to_string()).await.unwrap();
let keys = driver.keys_by_pattern("").await.unwrap();
assert!(keys.is_empty(), "Empty pattern should return no matches");
}
}