oxcache 0.5.0-rc.3

A high-performance multi-level cache library for Rust with L1 (memory) and L2 (Redis) caching.
Documentation
// Copyright (c) 2025-2026 Kirky.X
// SPDX-License-Identifier: MIT
//! Cache 宏注册和 Lua 脚本方法

use super::Cache;
use crate::error::{OxCacheError, OxCacheResult};
use crate::traits::CacheKey;
use std::sync::Arc;

impl Cache<String, Vec<u8>> {
    /// Register this cache for use with the `#[cached]` macro.
    ///
    /// Only `Cache<String, Vec<u8>>` can be registered for macro usage.
    ///
    /// # Arguments
    ///
    /// * `service_name` - Unique service name for the macro
    ///
    /// # Returns
    ///
    /// * `Ok(())` - Registration successful
    /// * `Err(OxCacheError)` - Registration failed
    pub async fn register_for_macro(&self, service_name: &str) -> OxCacheResult<()> {
        use crate::internal::__internal_register_cache;

        if service_name.is_empty() {
            return Err(OxCacheError::InvalidInput(
                "service_name must not be empty".to_string(),
            ));
        }

        let backend = self.backend.clone();
        let mut cache: Cache<String, Vec<u8>> = Cache::new_with_backend(backend);
        // Propagate backend_sync so the #[cached(sync)] macro can use sync byte ops.
        if let Some(sync_backend) = &self.backend_sync {
            cache.set_sync_backend(sync_backend.clone());
        }
        // Preserve per-instance configuration: new_with_backend resets both to
        // defaults, which would silently downgrade the registered clone.
        cache.set_null_cache_ttl(self.null_cache_ttl);
        cache.set_ttl_jitter_factor(self.ttl_jitter_factor);
        __internal_register_cache(service_name, Arc::new(cache));
        Ok(())
    }
}

impl<K, V> Cache<K, V>
where
    K: CacheKey,
    V: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
    #[cfg(feature = "lua")]
    pub async fn eval_lua(
        &self,
        script: &str,
        keys: &[&str],
        args: &[&str],
    ) -> OxCacheResult<redis::Value> {
        let executor = self.backend.as_lua_executor().ok_or_else(|| {
            OxCacheError::Operation(
                "Lua scripts require a Redis backend. Current backend does not support Lua execution.".to_string(),
            )
        })?;
        executor.eval_lua(script, keys, args).await
    }

    #[cfg(feature = "lua")]
    pub async fn eval_sha(
        &self,
        sha: &str,
        keys: &[&str],
        args: &[&str],
    ) -> OxCacheResult<redis::Value> {
        let executor = self.backend.as_lua_executor().ok_or_else(|| {
            OxCacheError::Operation(
                "Lua scripts require a Redis backend. Current backend does not support Lua execution.".to_string(),
            )
        })?;
        executor.eval_sha(sha, keys, args).await
    }

    #[cfg(feature = "lua")]
    pub async fn script_load(&self, script: &str) -> OxCacheResult<String> {
        let executor = self.backend.as_lua_executor().ok_or_else(|| {
            OxCacheError::Operation(
                "Lua scripts require a Redis backend. Current backend does not support Lua execution.".to_string(),
            )
        })?;
        executor.script_load(script).await
    }
}

#[cfg(test)]
mod tests {
    use crate::cache::Cache;
    #[cfg(feature = "lua")]
    use crate::error::OxCacheError;

    // ========================================================================
    // register_for_macro tests
    // ========================================================================

    #[tokio::test]
    async fn test_register_for_macro_string_vec_u8() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        let result = cache.register_for_macro("test_service").await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_register_for_macro_multiple_services() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        assert!(cache.register_for_macro("svc_a").await.is_ok());
        assert!(cache.register_for_macro("svc_b").await.is_ok());
    }

    #[tokio::test]
    async fn test_register_for_macro_empty_service_name() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        let result = cache.register_for_macro("").await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_register_for_macro_preserves_builder_config() {
        use crate::internal::__internal_get_cache;
        use std::time::Duration;

        // The registered clone must keep the original's per-instance config
        // (null_cache_ttl / ttl_jitter), not the new_with_backend defaults.
        let cache: Cache<String, Vec<u8>> = Cache::builder()
            .null_cache_ttl(Duration::from_secs(60))
            .ttl_jitter(0.2)
            .build()
            .await
            .unwrap();
        cache.register_for_macro("cfg_preserve_svc").await.unwrap();

        let registered = __internal_get_cache("cfg_preserve_svc")
            .expect("registered cache should be retrievable");
        assert_eq!(registered.null_cache_ttl(), Some(Duration::from_secs(60)));
        assert!((registered.ttl_jitter_factor() - 0.2).abs() < 1e-9);
    }

    // ========================================================================
    // Lua script feature-gated tests
    // ========================================================================

    #[tokio::test]
    #[cfg(feature = "lua")]
    async fn test_eval_lua_returns_error_on_non_redis_backend() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        let result = cache.eval_lua("return 1", &[], &[]).await;
        assert!(result.is_err());
        let err = result.unwrap_err();
        match &err {
            OxCacheError::Operation(msg) => {
                assert!(msg.contains("Lua scripts require a Redis backend"));
            }
            _ => panic!("Expected Operation error, got {:?}", err),
        }
    }

    #[tokio::test]
    #[cfg(feature = "lua")]
    async fn test_eval_sha_returns_error_on_non_redis_backend() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        let result = cache.eval_sha("abc123", &[], &[]).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    #[cfg(feature = "lua")]
    async fn test_script_load_returns_error_on_non_redis_backend() {
        let cache: Cache<String, Vec<u8>> = Cache::memory().await.unwrap();
        let result = cache.script_load("return 1").await;
        assert!(result.is_err());
    }
}