rig-redis 0.1.0

Redis support utilities for Rig integrations.
Documentation
//! Redis support utilities for Rig integrations.
//!
//! The crate intentionally starts with client-agnostic configuration and key
//! helpers. That keeps the public surface useful for Redis-backed Rig code
//! without forcing a specific async runtime or Redis client choice.

use std::fmt;

const DEFAULT_NAMESPACE: &str = "rig";
const DEFAULT_INDEX: &str = "rig:index";

/// Redis connection and namespace settings.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RedisConfig {
    url: String,
    namespace: String,
    index: String,
}

impl RedisConfig {
    /// Creates a new Redis configuration using the default namespace and index.
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            namespace: DEFAULT_NAMESPACE.to_string(),
            index: DEFAULT_INDEX.to_string(),
        }
    }

    /// Returns the Redis connection URL.
    pub fn url(&self) -> &str {
        &self.url
    }

    /// Returns the namespace prefix used for generated Redis keys.
    pub fn namespace(&self) -> &str {
        &self.namespace
    }

    /// Returns the Redis index name.
    pub fn index(&self) -> &str {
        &self.index
    }

    /// Sets the namespace prefix used for generated Redis keys.
    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
        self.namespace = namespace.into();
        self
    }

    /// Sets the Redis index name.
    pub fn with_index(mut self, index: impl Into<String>) -> Self {
        self.index = index.into();
        self
    }
}

/// A Redis key with a namespace prefix.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RedisKey(String);

impl RedisKey {
    /// Creates a namespaced Redis key.
    pub fn new(namespace: impl AsRef<str>, key: impl AsRef<str>) -> Self {
        let namespace = namespace.as_ref().trim_matches(':');
        let key = key.as_ref().trim_matches(':');

        if namespace.is_empty() {
            Self(key.to_string())
        } else if key.is_empty() {
            Self(namespace.to_string())
        } else {
            Self(format!("{namespace}:{key}"))
        }
    }

    /// Returns the key as a string slice.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Consumes the key and returns the underlying string.
    pub fn into_string(self) -> String {
        self.0
    }
}

impl fmt::Display for RedisKey {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

impl AsRef<str> for RedisKey {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl From<RedisKey> for String {
    fn from(key: RedisKey) -> Self {
        key.into_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn config_uses_defaults() {
        let config = RedisConfig::new("redis://localhost:6379");

        assert_eq!(config.url(), "redis://localhost:6379");
        assert_eq!(config.namespace(), "rig");
        assert_eq!(config.index(), "rig:index");
    }

    #[test]
    fn config_can_be_customized() {
        let config = RedisConfig::new("redis://localhost:6379")
            .with_namespace("agents")
            .with_index("agent-memory");

        assert_eq!(config.namespace(), "agents");
        assert_eq!(config.index(), "agent-memory");
    }

    #[test]
    fn key_joins_namespace_and_key() {
        let key = RedisKey::new("rig", "documents:1");

        assert_eq!(key.as_str(), "rig:documents:1");
        assert_eq!(key.to_string(), "rig:documents:1");
    }

    #[test]
    fn key_trims_extra_separators() {
        let key = RedisKey::new("rig:", ":documents:1:");

        assert_eq!(key.as_str(), "rig:documents:1");
    }

    #[test]
    fn key_allows_empty_namespace() {
        let key = RedisKey::new("", "documents:1");

        assert_eq!(key.as_str(), "documents:1");
    }
}