mecha10-core 0.8.0

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

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

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)
    }

    /// Multiplexed connection to this backend's Redis client.
    ///
    /// Every `SecretBackend` method below needs one before it can do anything else; factoring
    /// the connect step out here collapses what would otherwise be a near-identical 5-line
    /// block repeated at the top of `get`/`set`/`delete`/`list`.
    async fn connection(&self) -> Result<redis::aio::MultiplexedConnection> {
        self.client
            .get_multiplexed_async_connection()
            .await
            .map_err(|e| Mecha10Error::Other(format!("Failed to get Redis connection: {}", e)))
    }
}

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

        let mut conn = self.connection().await?;

        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.connection().await?;

        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.connection().await?;

        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.connection().await?;

        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)))?;

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

        Ok(keys)
    }
}