mecha10-core 0.6.3

Mecha10 core runtime and foundational types
Documentation
//! Redis Secret Backend

use super::SecretBackend;
use crate::{Mecha10Error, Result};

/// Secret backend using Redis for storage
///
/// Stores secrets in Redis with optional TTL. Useful for temporary secrets
/// and distributed systems.
///
/// # Example
///
/// ```rust
/// use mecha10_core::secrets::{RedisBackend, SecretBackend};
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let backend = RedisBackend::connect("redis://localhost:6379", "secrets:").await?;
///
/// // Store a secret
/// backend.set("api_key", "secret_value").await?;
///
/// // Retrieve it
/// let api_key = backend.get("api_key").await?;
/// # Ok(())
/// # }
/// ```
pub struct RedisBackend {
    client: redis::Client,
    prefix: String,
}

impl RedisBackend {
    /// Connect to Redis
    ///
    /// # Arguments
    ///
    /// * `url` - Redis connection URL
    /// * `prefix` - Key prefix for secrets (e.g., "secrets:")
    pub async fn connect(url: &str, prefix: &str) -> Result<Self> {
        let client = redis::Client::open(url)
            .map_err(|e| Mecha10Error::Configuration(format!("Failed to connect to Redis: {}", e)))?;

        Ok(Self {
            client,
            prefix: prefix.to_string(),
        })
    }

    /// Connect using REDIS_URL environment variable
    pub async fn from_env(prefix: &str) -> Result<Self> {
        let url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://localhost:6379".to_string());
        Self::connect(&url, prefix).await
    }

    fn make_key(&self, key: &str) -> String {
        format!("{}{}", self.prefix, key)
    }
}

#[async_trait::async_trait]
impl SecretBackend for RedisBackend {
    async fn get(&self, key: &str) -> Result<String> {
        use redis::AsyncCommands;

        let mut conn = self
            .client
            .get_multiplexed_async_connection()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get Redis connection: {}", e)))?;

        let redis_key = self.make_key(key);
        let value: Option<String> = conn
            .get(&redis_key)
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get secret from Redis: {}", e)))?;

        value.ok_or_else(|| Mecha10Error::Configuration(format!("Secret '{}' not found in Redis", key)))
    }

    async fn set(&self, key: &str, value: &str) -> Result<()> {
        use redis::AsyncCommands;

        let mut conn = self
            .client
            .get_multiplexed_async_connection()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get Redis connection: {}", e)))?;

        let redis_key = self.make_key(key);
        conn.set::<_, _, ()>(&redis_key, value)
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to set secret in Redis: {}", e)))?;

        Ok(())
    }

    async fn delete(&self, key: &str) -> Result<()> {
        use redis::AsyncCommands;

        let mut conn = self
            .client
            .get_multiplexed_async_connection()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get Redis connection: {}", e)))?;

        let redis_key = self.make_key(key);
        conn.del::<_, ()>(&redis_key)
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to delete secret from Redis: {}", e)))?;

        Ok(())
    }

    async fn list(&self) -> Result<Vec<String>> {
        use redis::AsyncCommands;

        let mut conn = self
            .client
            .get_multiplexed_async_connection()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get Redis connection: {}", e)))?;

        let pattern = format!("{}*", self.prefix);
        let keys: Vec<String> = conn
            .keys(&pattern)
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to list secrets from Redis: {}", e)))?;

        // Strip prefix from keys
        let prefix_len = self.prefix.len();
        let keys = keys.into_iter().map(|k| k[prefix_len..].to_string()).collect();

        Ok(keys)
    }
}