kegani 0.1.0

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Redis client for Kegani
//!
//! Provides async Redis connection and caching utilities.

use redis::{AsyncCommands, Client, aio::ConnectionManager};
use std::time::Duration;

/// Redis configuration
#[derive(Debug, Clone)]
pub struct RedisConfig {
    pub url: String,
    pub pool_size: u32,
    pub timeout: Duration,
    pub prefix: String,
}

impl Default for RedisConfig {
    fn default() -> Self {
        Self {
            url: std::env::var("REDIS_URL")
                .unwrap_or_else(|_| "redis://localhost:6379".to_string()),
            pool_size: 5,
            timeout: Duration::from_secs(5),
            prefix: "kegani:".to_string(),
        }
    }
}

impl RedisConfig {
    /// Create from URL
    pub fn new(url: &str) -> Self {
        Self {
            url: url.to_string(),
            ..Default::default()
        }
    }

    /// Set pool size
    pub fn pool_size(mut self, size: u32) -> Self {
        self.pool_size = size;
        self
    }

    /// Set prefix for all keys
    pub fn prefix(mut self, prefix: &str) -> Self {
        self.prefix = prefix.to_string();
        self
    }
}

/// Redis cache client
#[derive(Clone)]
pub struct RedisCache {
    connection: ConnectionManager,
    config: RedisConfig,
}

impl RedisCache {
    /// Create a new Redis cache client
    pub async fn new(config: RedisConfig) -> Result<Self, redis::RedisError> {
        let client = Client::open(config.url.clone())?;
        let connection = ConnectionManager::new(client).await?;

        Ok(Self { connection, config })
    }

    /// Create from URL
    pub async fn connect(url: &str) -> Result<Self, redis::RedisError> {
        Self::new(RedisConfig::new(url)).await
    }

    /// Get a value
    pub async fn get(&self, key: &str) -> Result<Option<String>, redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);
        conn.get(&prefixed_key).await
    }

    /// Get and deserialize JSON
    pub async fn get_json<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>, CacheError> {
        match self.get(key).await {
            Ok(Some(value)) => {
                serde_json::from_str(&value).map_err(CacheError::Deserialize)
            }
            Ok(None) => Ok(None),
            Err(e) => Err(CacheError::Redis(e)),
        }
    }

    /// Set a value with expiration
    pub async fn set(&self, key: &str, value: &str, ttl: Option<Duration>) -> Result<(), redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);

        if let Some(duration) = ttl {
            conn.set_ex(&prefixed_key, value, duration.as_secs()).await
        } else {
            conn.set(&prefixed_key, value).await
        }
    }

    /// Set JSON value
    pub async fn set_json<T: serde::Serialize>(&self, key: &str, value: &T, ttl: Option<Duration>) -> Result<(), CacheError> {
        let json = serde_json::to_string(value).map_err(CacheError::Serialize)?;
        self.set(key, &json, ttl).await.map_err(CacheError::Redis)
    }

    /// Delete a key
    pub async fn delete(&self, key: &str) -> Result<(), redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);
        conn.del(&prefixed_key).await
    }

    /// Delete by pattern
    pub async fn delete_pattern(&self, pattern: &str) -> Result<u64, redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_pattern = format!("{}{}", self.config.prefix, pattern);

        let keys: Vec<String> = conn.keys(&prefixed_pattern).await?;
        if keys.is_empty() {
            return Ok(0);
        }
        conn.del(keys).await
    }

    /// Check if key exists
    pub async fn exists(&self, key: &str) -> Result<bool, redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);
        conn.exists(&prefixed_key).await
    }

    /// Set expiration
    pub async fn expire(&self, key: &str, ttl: Duration) -> Result<(), redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);
        conn.expire(&prefixed_key, ttl.as_secs() as i64).await
    }

    /// Increment a counter
    pub async fn incr(&self, key: &str) -> Result<i64, redis::RedisError> {
        let mut conn = self.connection.clone();
        let prefixed_key = format!("{}{}", self.config.prefix, key);
        conn.incr(&prefixed_key, 1).await
    }

    /// Get or set with callback
    pub async fn get_or_set<F, Fut, T>(&self, key: &str, ttl: Duration, fetch: F) -> Result<T, CacheError>
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = Result<T, CacheError>>,
        T: serde::de::DeserializeOwned + serde::Serialize,
    {
        // Try to get from cache
        if let Some(cached) = self.get_json::<T>(key).await? {
            return Ok(cached);
        }

        // Fetch from source
        let value = fetch().await?;

        // Store in cache
        self.set_json(key, &value, Some(ttl)).await?;

        Ok(value)
    }

    /// Ping Redis
    pub async fn ping(&self) -> Result<(), redis::RedisError> {
        let mut conn = self.connection.clone();
        redis::cmd("PING").query_async::<String>(&mut conn).await?;
        Ok(())
    }
}

/// Cache error
#[derive(Debug)]
pub enum CacheError {
    Redis(redis::RedisError),
    Serialize(serde_json::Error),
    Deserialize(serde_json::Error),
}

impl std::fmt::Display for CacheError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CacheError::Redis(e) => write!(f, "Redis error: {}", e),
            CacheError::Serialize(e) => write!(f, "Serialization error: {}", e),
            CacheError::Deserialize(e) => write!(f, "Deserialization error: {}", e),
        }
    }
}

impl std::error::Error for CacheError {}

impl From<redis::RedisError> for CacheError {
    fn from(e: redis::RedisError) -> Self {
        CacheError::Redis(e)
    }
}

impl From<serde_json::Error> for CacheError {
    fn from(e: serde_json::Error) -> Self {
        CacheError::Serialize(e)
    }
}