use crate::infrastructure::config::redis::RedisConfig;
use redis::{Client, Commands, Connection, RedisError, RedisResult};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, instrument};
pub struct RedisClient {
connection_pool: Arc<Mutex<Connection>>,
config: RedisConfig,
}
impl RedisClient {
#[instrument(skip(config), level = "debug")]
pub fn new(config: RedisConfig) -> Result<Self, RedisError> {
let url = config.url();
info!("Connecting to Redis at {}", url);
let client = Client::open(url)?;
let connection = client.get_connection()?;
let connection_pool = Arc::new(Mutex::new(connection));
info!("Successfully connected to Redis");
Ok(Self {
connection_pool,
config,
})
}
fn get_connection(&self) -> Result<std::sync::MutexGuard<'_, Connection>, RedisError> {
self.connection_pool.lock().map_err(|e| {
error!("Failed to acquire Redis connection lock: {}", e);
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})
}
#[instrument(skip(self), level = "debug")]
pub fn get<T: redis::FromRedisValue>(&self, key: &str) -> RedisResult<Option<T>> {
let mut connection = self.get_connection()?;
let exists: bool = connection.exists(key)?;
if !exists {
debug!("Key '{}' not found in Redis", key);
return Ok(None);
}
debug!("Retrieving key '{}' from Redis", key);
let value: T = connection.get(key)?;
Ok(Some(value))
}
#[instrument(skip(self, value), level = "debug")]
pub fn set<T: redis::ToRedisArgs>(
&self,
key: &str,
value: T,
expiry_secs: Option<u64>,
) -> RedisResult<()> {
let mut connection = self.get_connection()?;
match expiry_secs {
Some(secs) => {
debug!(
"Setting key '{}' in Redis with expiry of {} seconds",
key, secs
);
connection.set_ex(key, value, secs)
}
None => {
debug!("Setting key '{}' in Redis without expiry", key);
connection.set(key, value)
}
}
}
#[instrument(skip(self), level = "debug")]
pub fn delete(&self, key: &str) -> RedisResult<bool> {
let mut connection = self.get_connection()?;
debug!("Deleting key '{}' from Redis", key);
let deleted: i32 = connection.del(key)?;
Ok(deleted > 0)
}
#[instrument(skip(self), level = "debug")]
pub fn keys(&self, pattern: &str) -> RedisResult<Vec<String>> {
let mut connection = self.get_connection()?;
debug!("Getting keys matching pattern '{}' from Redis", pattern);
connection.keys(pattern)
}
#[instrument(skip(self), level = "debug")]
pub fn exists(&self, key: &str) -> RedisResult<bool> {
let mut connection = self.get_connection()?;
debug!("Checking if key '{}' exists in Redis", key);
connection.exists(key)
}
pub fn get_config(&self) -> &RedisConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use redis::{RedisError, RedisResult};
use std::sync::{Arc, Mutex};
struct MockConnection {
exists_results: std::collections::HashMap<String, bool>,
get_results: std::collections::HashMap<String, String>,
del_results: std::collections::HashMap<String, i32>,
keys_results: std::collections::HashMap<String, Vec<String>>,
}
impl MockConnection {
fn new() -> Self {
Self {
exists_results: std::collections::HashMap::new(),
get_results: std::collections::HashMap::new(),
del_results: std::collections::HashMap::new(),
keys_results: std::collections::HashMap::new(),
}
}
fn with_exists(mut self, key: &str, result: bool) -> Self {
self.exists_results.insert(key.to_string(), result);
self
}
fn with_get(mut self, key: &str, result: &str) -> Self {
self.get_results.insert(key.to_string(), result.to_string());
self
}
fn with_del(mut self, key: &str, result: i32) -> Self {
self.del_results.insert(key.to_string(), result);
self
}
fn with_keys(mut self, pattern: &str, result: Vec<String>) -> Self {
self.keys_results.insert(pattern.to_string(), result);
self
}
fn exists(&mut self, key: &str) -> RedisResult<bool> {
match self.exists_results.get(key) {
Some(result) => Ok(*result),
None => Err(RedisError::from((
redis::ErrorKind::TypeError,
"Key not configured in mock",
))),
}
}
fn get(&mut self, key: &str) -> RedisResult<String> {
match self.get_results.get(key) {
Some(value) => Ok(value.clone()),
None => Err(RedisError::from((
redis::ErrorKind::TypeError,
"Key not configured in mock",
))),
}
}
fn set(&mut self, _key: &str, _value: &str) -> RedisResult<()> {
Ok(())
}
fn set_ex(&mut self, _key: &str, _value: &str, _seconds: u64) -> RedisResult<()> {
Ok(())
}
fn del(&mut self, key: &str) -> RedisResult<i32> {
match self.del_results.get(key) {
Some(result) => Ok(*result),
None => Err(RedisError::from((
redis::ErrorKind::TypeError,
"Key not configured in mock",
))),
}
}
fn keys(&mut self, pattern: &str) -> RedisResult<Vec<String>> {
match self.keys_results.get(pattern) {
Some(result) => Ok(result.clone()),
None => Err(RedisError::from((
redis::ErrorKind::TypeError,
"Pattern not configured in mock",
))),
}
}
}
struct TestRedisClient {
connection: Arc<Mutex<MockConnection>>,
config: RedisConfig,
}
impl TestRedisClient {
fn get<T: std::string::ToString + std::fmt::Display>(
&self,
key: T,
) -> RedisResult<Option<String>> {
let key_str = key.to_string();
let mut connection = self.connection.lock().map_err(|e| {
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})?;
let exists = connection.exists(&key_str)?;
if !exists {
return Ok(None);
}
let value = connection.get(&key_str)?;
Ok(Some(value))
}
fn set<T: std::string::ToString, V: std::string::ToString>(
&self,
key: T,
value: V,
expiry_secs: Option<u64>,
) -> RedisResult<()> {
let key_str = key.to_string();
let value_str = value.to_string();
let mut connection = self.connection.lock().map_err(|e| {
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})?;
match expiry_secs {
Some(secs) => connection.set_ex(&key_str, &value_str, secs),
None => connection.set(&key_str, &value_str),
}
}
fn delete<T: std::string::ToString>(&self, key: T) -> RedisResult<bool> {
let key_str = key.to_string();
let mut connection = self.connection.lock().map_err(|e| {
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})?;
let deleted = connection.del(&key_str)?;
Ok(deleted > 0)
}
fn keys<T: std::string::ToString>(&self, pattern: T) -> RedisResult<Vec<String>> {
let pattern_str = pattern.to_string();
let mut connection = self.connection.lock().map_err(|e| {
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})?;
connection.keys(&pattern_str)
}
fn exists<T: std::string::ToString>(&self, key: T) -> RedisResult<bool> {
let key_str = key.to_string();
let mut connection = self.connection.lock().map_err(|e| {
RedisError::from(std::io::Error::other(format!(
"Failed to acquire Redis connection lock: {}",
e
)))
})?;
connection.exists(&key_str)
}
fn get_config(&self) -> &RedisConfig {
&self.config
}
}
#[test]
fn test_get_existing_key() {
let mock_conn = MockConnection::new()
.with_exists("test_key", true)
.with_get("test_key", "test_value");
let client = create_test_client(mock_conn);
let result = client.get("test_key");
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some("test_value".to_string()));
}
#[test]
fn test_get_non_existing_key() {
let mock_conn = MockConnection::new().with_exists("non_existing_key", false);
let client = create_test_client(mock_conn);
let result = client.get("non_existing_key");
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_set_without_expiry() {
let mock_conn = MockConnection::new();
let client = create_test_client(mock_conn);
let result = client.set("test_key", "test_value", None);
assert!(result.is_ok());
}
#[test]
fn test_set_with_expiry() {
let mock_conn = MockConnection::new();
let client = create_test_client(mock_conn);
let result = client.set("test_key", "test_value", Some(60));
assert!(result.is_ok());
}
#[test]
fn test_delete_existing_key() {
let mock_conn = MockConnection::new().with_del("test_key", 1);
let client = create_test_client(mock_conn);
let result = client.delete("test_key");
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_delete_non_existing_key() {
let mock_conn = MockConnection::new().with_del("non_existing_key", 0);
let client = create_test_client(mock_conn);
let result = client.delete("non_existing_key");
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_keys() {
let mock_conn = MockConnection::new()
.with_keys("test*", vec!["test1".to_string(), "test2".to_string()]);
let client = create_test_client(mock_conn);
let result = client.keys("test*");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
vec!["test1".to_string(), "test2".to_string()]
);
}
#[test]
fn test_exists_true() {
let mock_conn = MockConnection::new().with_exists("existing_key", true);
let client = create_test_client(mock_conn);
let result = client.exists("existing_key");
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_exists_false() {
let mock_conn = MockConnection::new().with_exists("non_existing_key", false);
let client = create_test_client(mock_conn);
let result = client.exists("non_existing_key");
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_get_config() {
let config = RedisConfig {
host: "test-host".to_string(),
port: 6380,
username: Some("testuser".to_string()),
password: Some("testpass".to_string()),
database: 2,
timeout: 60,
};
let mock_conn = MockConnection::new();
let client = TestRedisClient {
connection: Arc::new(Mutex::new(mock_conn)),
config: config.clone(),
};
let result = client.get_config();
assert_eq!(result.host, "test-host");
assert_eq!(result.port, 6380);
assert_eq!(result.username, Some("testuser".to_string()));
assert_eq!(result.password, Some("testpass".to_string()));
assert_eq!(result.database, 2);
assert_eq!(result.timeout, 60);
}
fn create_test_client(mock_conn: MockConnection) -> TestRedisClient {
let config = RedisConfig::default();
let _client = Client::open(format!("redis://{}:{}", config.host, config.port))
.expect("Failed to create Redis client");
TestRedisClient {
connection: Arc::new(Mutex::new(mock_conn)),
config,
}
}
#[test]
fn test_get_connection_error() {
use std::sync::{Arc, Mutex};
use std::thread;
let connection = Arc::new(Mutex::new(MockConnection::new()));
let connection_clone = Arc::clone(&connection);
let handle = thread::spawn(move || {
let _lock = connection_clone.lock().unwrap();
panic!("Intentionally poisoning the mutex");
});
let _ = handle.join();
let config = RedisConfig::default();
let client = TestRedisClient { connection, config };
let result = client.get("test_key");
assert!(result.is_err());
}
}