diff --git a/modules/cache/go.mod b/modules/cache/go.mod index 34ec7b2a..c57ca953 100644 --- a/modules/cache/go.mod +++ b/modules/cache/go.mod @@ -6,16 +6,21 @@ toolchain go1.24.3 require ( github.com/GoCodeAlone/modular v1.3.0 + github.com/alicebob/miniredis/v2 v2.35.0 + github.com/redis/go-redis/v9 v9.10.0 github.com/stretchr/testify v1.10.0 ) require ( github.com/BurntSushi/toml v1.5.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golobby/cast v1.3.3 // indirect github.com/golobby/config/v3 v3.4.2 // indirect github.com/golobby/dotenv v1.3.2 // indirect github.com/golobby/env/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/modules/cache/go.sum b/modules/cache/go.sum index cb8c11f6..7c90c215 100644 --- a/modules/cache/go.sum +++ b/modules/cache/go.sum @@ -3,11 +3,21 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/GoCodeAlone/modular v1.3.0 h1:KVny0S447hTUXYY8Y7BltL94poN/ZtaH3v2V7jU7d3o= github.com/GoCodeAlone/modular v1.3.0/go.mod h1:dD1xYmBQdtYahsrdwP1DAe2Tz6SkCXA8foairMuY3Pk= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/golobby/cast v1.3.3 h1:s2Lawb9RMz7YyYf8IrfMQY4IFmA1R/lgfmj97Vc6fig= github.com/golobby/cast v1.3.3/go.mod h1:0oDO5IT84HTXcbLDf1YXuk0xtg/cRDrxhbpWKxwtJCY= github.com/golobby/config/v3 v3.4.2 h1:oIOSo24mC0A8f93ZTL24NDNw0hZ3Tbb34wc1ckn2CsA= @@ -27,6 +37,8 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= +github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= @@ -40,6 +52,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/modules/cache/memory.go b/modules/cache/memory.go index 54af6cc5..b81bf5fd 100644 --- a/modules/cache/memory.go +++ b/modules/cache/memory.go @@ -30,9 +30,11 @@ func NewMemoryCache(config *CacheConfig) *MemoryCache { // Connect initializes the memory cache func (c *MemoryCache) Connect(ctx context.Context) error { - // Start cleanup goroutine + // Start cleanup goroutine with derived context c.cleanupCtx, c.cancelFunc = context.WithCancel(ctx) - go c.startCleanupTimer(c.cleanupCtx) + go func() { + c.startCleanupTimer(c.cleanupCtx) + }() return nil } diff --git a/modules/cache/module.go b/modules/cache/module.go index e51a794a..609c38f9 100644 --- a/modules/cache/module.go +++ b/modules/cache/module.go @@ -2,6 +2,7 @@ package cache import ( "context" + "fmt" "time" "github.com/GoCodeAlone/modular" @@ -56,7 +57,7 @@ func (m *CacheModule) Init(app modular.Application) error { // Retrieve the registered config section for access cfg, err := app.GetConfigSection(m.name) if err != nil { - return err + return fmt.Errorf("failed to get config section for cache module: %w", err) } m.config = cfg.GetConfig().(*CacheConfig) @@ -84,7 +85,7 @@ func (m *CacheModule) Start(ctx context.Context) error { m.logger.Info("Starting cache module") err := m.cacheEngine.Connect(ctx) if err != nil { - return err + return fmt.Errorf("failed to connect cache engine: %w", err) } return nil } @@ -92,7 +93,10 @@ func (m *CacheModule) Start(ctx context.Context) error { // Stop performs shutdown logic for the module func (m *CacheModule) Stop(ctx context.Context) error { m.logger.Info("Stopping cache module") - return m.cacheEngine.Close(ctx) + if err := m.cacheEngine.Close(ctx); err != nil { + return fmt.Errorf("failed to close cache engine: %w", err) + } + return nil } // Dependencies returns the names of modules this module depends on @@ -133,22 +137,35 @@ func (m *CacheModule) Set(ctx context.Context, key string, value interface{}, tt if ttl == 0 { ttl = time.Duration(m.config.DefaultTTL) * time.Second } - return m.cacheEngine.Set(ctx, key, value, ttl) + if err := m.cacheEngine.Set(ctx, key, value, ttl); err != nil { + return fmt.Errorf("failed to set cache item: %w", err) + } + return nil } // Delete removes an item from the cache func (m *CacheModule) Delete(ctx context.Context, key string) error { - return m.cacheEngine.Delete(ctx, key) + if err := m.cacheEngine.Delete(ctx, key); err != nil { + return fmt.Errorf("failed to delete cache item: %w", err) + } + return nil } // Flush removes all items from the cache func (m *CacheModule) Flush(ctx context.Context) error { - return m.cacheEngine.Flush(ctx) + if err := m.cacheEngine.Flush(ctx); err != nil { + return fmt.Errorf("failed to flush cache: %w", err) + } + return nil } // GetMulti retrieves multiple items from the cache func (m *CacheModule) GetMulti(ctx context.Context, keys []string) (map[string]interface{}, error) { - return m.cacheEngine.GetMulti(ctx, keys) + result, err := m.cacheEngine.GetMulti(ctx, keys) + if err != nil { + return nil, fmt.Errorf("failed to get multiple cache items: %w", err) + } + return result, nil } // SetMulti stores multiple items in the cache @@ -156,10 +173,16 @@ func (m *CacheModule) SetMulti(ctx context.Context, items map[string]interface{} if ttl == 0 { ttl = time.Duration(m.config.DefaultTTL) * time.Second } - return m.cacheEngine.SetMulti(ctx, items, ttl) + if err := m.cacheEngine.SetMulti(ctx, items, ttl); err != nil { + return fmt.Errorf("failed to set multiple cache items: %w", err) + } + return nil } // DeleteMulti removes multiple items from the cache func (m *CacheModule) DeleteMulti(ctx context.Context, keys []string) error { - return m.cacheEngine.DeleteMulti(ctx, keys) + if err := m.cacheEngine.DeleteMulti(ctx, keys); err != nil { + return fmt.Errorf("failed to delete multiple cache items: %w", err) + } + return nil } diff --git a/modules/cache/module_test.go b/modules/cache/module_test.go index 07200dd7..c8effa15 100644 --- a/modules/cache/module_test.go +++ b/modules/cache/module_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/GoCodeAlone/modular" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -218,3 +219,349 @@ func TestExpiration(t *testing.T) { err = module.Stop(ctx) require.NoError(t, err) } + +// TestRedisConfiguration tests Redis configuration handling without actual Redis connection +func TestRedisConfiguration(t *testing.T) { + // Create the module + module := NewModule().(*CacheModule) + + // Initialize with Redis config + app := newMockApp() + err := module.RegisterConfig(app) + require.NoError(t, err) + + // Override config for Redis + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://localhost:6379", + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + app.RegisterConfigSection(ModuleName, modular.NewStdConfigProvider(config)) + + err = module.Init(app) + require.NoError(t, err) + + // Ensure we have a Redis cache + assert.IsType(t, &RedisCache{}, module.cacheEngine) + + // Note: We don't start the module here as it would require an actual Redis connection +} + +// TestRedisOperationsWithMockBehavior tests Redis cache operations that don't require a real connection +func TestRedisOperationsWithMockBehavior(t *testing.T) { + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://localhost:6379", + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Test operations without connection (should return appropriate errors) + _, found := cache.Get(ctx, "test-key") + assert.False(t, found) + + err := cache.Set(ctx, "test-key", "test-value", time.Minute) + assert.Equal(t, ErrNotConnected, err) + + err = cache.Delete(ctx, "test-key") + assert.Equal(t, ErrNotConnected, err) + + err = cache.Flush(ctx) + assert.Equal(t, ErrNotConnected, err) + + _, err = cache.GetMulti(ctx, []string{"key1", "key2"}) + assert.Equal(t, ErrNotConnected, err) + + err = cache.SetMulti(ctx, map[string]interface{}{"key1": "value1"}, time.Minute) + assert.Equal(t, ErrNotConnected, err) + + err = cache.DeleteMulti(ctx, []string{"key1", "key2"}) + assert.Equal(t, ErrNotConnected, err) + + // Test close without connection + err = cache.Close(ctx) + assert.NoError(t, err) +} + +// TestRedisConfigurationEdgeCases tests edge cases in Redis configuration +func TestRedisConfigurationEdgeCases(t *testing.T) { + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "invalid-url", + RedisPassword: "test-password", + RedisDB: 1, + ConnectionMaxAge: 120, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Test connection with invalid URL + err := cache.Connect(ctx) + assert.Error(t, err) +} + +// TestRedisMultiOperationsEmptyInputs tests multi operations with empty inputs +func TestRedisMultiOperationsEmptyInputs(t *testing.T) { + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://localhost:6379", + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Test GetMulti with empty keys - should return empty map (no connection needed) + results, err := cache.GetMulti(ctx, []string{}) + assert.NoError(t, err) + assert.Equal(t, map[string]interface{}{}, results) + + // Test SetMulti with empty items - should succeed (no connection needed) + err = cache.SetMulti(ctx, map[string]interface{}{}, time.Minute) + assert.NoError(t, err) + + // Test DeleteMulti with empty keys - should succeed (no connection needed) + err = cache.DeleteMulti(ctx, []string{}) + assert.NoError(t, err) +} + +// TestRedisConnectWithPassword tests connection configuration with password +func TestRedisConnectWithPassword(t *testing.T) { + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://localhost:6379", + RedisPassword: "test-password", + RedisDB: 1, + ConnectionMaxAge: 120, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Test connection with password and different DB - this will fail since no Redis server + // but will exercise the connection configuration code paths + err := cache.Connect(ctx) + assert.Error(t, err) // Expected to fail without Redis server + + // Test Close when client is nil initially + err = cache.Close(ctx) + assert.NoError(t, err) +} + +// TestRedisJSONMarshaling tests JSON marshaling error scenarios +func TestRedisJSONMarshaling(t *testing.T) { + // Start a test Redis server + s := miniredis.RunT(t) + defer s.Close() + + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://" + s.Addr(), + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Connect to the test Redis server + err := cache.Connect(ctx) + require.NoError(t, err) + defer cache.Close(ctx) + + // Test Set with invalid JSON value (function cannot be marshaled) + err = cache.Set(ctx, "test-key", func() {}, time.Minute) + assert.Equal(t, ErrInvalidValue, err) + + // Test SetMulti with values that cause JSON marshaling errors + invalidItems := map[string]interface{}{ + "valid-key": "valid-value", + "invalid-key": func() {}, // Functions cannot be marshaled to JSON + } + + err = cache.SetMulti(ctx, invalidItems, time.Minute) + assert.Equal(t, ErrInvalidValue, err) +} + +// TestRedisFullOperations tests Redis operations with a test server +func TestRedisFullOperations(t *testing.T) { + // Start a test Redis server + s := miniredis.RunT(t) + defer s.Close() + + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://" + s.Addr(), + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Test Connect + err := cache.Connect(ctx) + require.NoError(t, err) + + // Test Set and Get + err = cache.Set(ctx, "test-key", "test-value", time.Minute) + assert.NoError(t, err) + + value, found := cache.Get(ctx, "test-key") + assert.True(t, found) + assert.Equal(t, "test-value", value) + + // Test Delete + err = cache.Delete(ctx, "test-key") + assert.NoError(t, err) + + _, found = cache.Get(ctx, "test-key") + assert.False(t, found) + + // Test SetMulti and GetMulti + items := map[string]interface{}{ + "key1": "value1", + "key2": 42, + "key3": map[string]string{"nested": "value"}, + } + + err = cache.SetMulti(ctx, items, time.Minute) + assert.NoError(t, err) + + results, err := cache.GetMulti(ctx, []string{"key1", "key2", "key3", "nonexistent"}) + assert.NoError(t, err) + assert.Equal(t, "value1", results["key1"]) + assert.Equal(t, float64(42), results["key2"]) // JSON unmarshaling returns numbers as float64 + assert.Equal(t, map[string]interface{}{"nested": "value"}, results["key3"]) + assert.NotContains(t, results, "nonexistent") + + // Test DeleteMulti + err = cache.DeleteMulti(ctx, []string{"key1", "key2"}) + assert.NoError(t, err) + + // Verify deletions + _, found = cache.Get(ctx, "key1") + assert.False(t, found) + _, found = cache.Get(ctx, "key2") + assert.False(t, found) + value, found = cache.Get(ctx, "key3") + assert.True(t, found) + assert.Equal(t, map[string]interface{}{"nested": "value"}, value) + + // Test Flush + err = cache.Flush(ctx) + assert.NoError(t, err) + + _, found = cache.Get(ctx, "key3") + assert.False(t, found) + + // Test Close + err = cache.Close(ctx) + assert.NoError(t, err) +} + +// TestRedisGetJSONUnmarshalError tests JSON unmarshaling errors in Get +func TestRedisGetJSONUnmarshalError(t *testing.T) { + // Start a test Redis server + s := miniredis.RunT(t) + defer s.Close() + + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://" + s.Addr(), + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Connect to the test Redis server + err := cache.Connect(ctx) + require.NoError(t, err) + defer cache.Close(ctx) + + // Manually insert invalid JSON into Redis + s.Set("invalid-json", "this is not valid JSON {") + + // Try to get the invalid JSON value + value, found := cache.Get(ctx, "invalid-json") + assert.False(t, found) + assert.Nil(t, value) +} + +// TestRedisGetWithServerError tests Get with server errors +func TestRedisGetWithServerError(t *testing.T) { + // Start a test Redis server + s := miniredis.RunT(t) + + config := &CacheConfig{ + Engine: "redis", + DefaultTTL: 300, + CleanupInterval: 60, + MaxItems: 10000, + RedisURL: "redis://" + s.Addr(), + RedisPassword: "", + RedisDB: 0, + ConnectionMaxAge: 60, + } + + cache := NewRedisCache(config) + ctx := context.Background() + + // Connect to the test Redis server + err := cache.Connect(ctx) + require.NoError(t, err) + + // Close the server to simulate connection error + s.Close() + + // Try to get a value when server is down + value, found := cache.Get(ctx, "test-key") + assert.False(t, found) + assert.Nil(t, value) + + // Try GetMulti when server is down + results, err := cache.GetMulti(ctx, []string{"key1", "key2"}) + assert.Error(t, err) + assert.Nil(t, results) + + // Close cache + cache.Close(ctx) +} diff --git a/modules/cache/redis.go b/modules/cache/redis.go index 8454a044..8c856abc 100644 --- a/modules/cache/redis.go +++ b/modules/cache/redis.go @@ -2,13 +2,18 @@ package cache import ( "context" + "encoding/json" + "errors" + "fmt" "time" + + "github.com/redis/go-redis/v9" ) // RedisCache implements CacheEngine using Redis type RedisCache struct { config *CacheConfig - client interface{} `json:"-"` // Placeholder for a Redis client; will be initialized in Connect + client *redis.Client } // NewRedisCache creates a new Redis cache engine @@ -20,65 +25,168 @@ func NewRedisCache(config *CacheConfig) *RedisCache { // Connect establishes connection to Redis func (c *RedisCache) Connect(ctx context.Context) error { - // Note: Actual implementation would initialize a Redis client - // This is a placeholder implementation that would be replaced - // when implementing a real Redis client + opts, err := redis.ParseURL(c.config.RedisURL) + if err != nil { + return fmt.Errorf("failed to parse Redis URL: %w", err) + } + + if c.config.RedisPassword != "" { + opts.Password = c.config.RedisPassword + } + + opts.DB = c.config.RedisDB + opts.ConnMaxLifetime = time.Duration(c.config.ConnectionMaxAge) * time.Second + + c.client = redis.NewClient(opts) + + // Test the connection + if err := c.client.Ping(ctx).Err(); err != nil { + return fmt.Errorf("failed to ping Redis server: %w", err) + } return nil } // Close closes the connection to Redis func (c *RedisCache) Close(ctx context.Context) error { - // Note: Actual implementation would close the Redis client + if c.client != nil { + if err := c.client.Close(); err != nil { + return fmt.Errorf("failed to close Redis connection: %w", err) + } + } return nil } // Get retrieves an item from the Redis cache func (c *RedisCache) Get(ctx context.Context, key string) (interface{}, bool) { - // Note: This is a placeholder implementation - // In a real implementation, we would: - // 1. Get the item from Redis - // 2. Deserialize the JSON data - // 3. Return the value - return nil, false + if c.client == nil { + return nil, false + } + + val, err := c.client.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, false + } + return nil, false + } + + var result interface{} + if err := json.Unmarshal([]byte(val), &result); err != nil { + return nil, false + } + + return result, true } // Set stores an item in the Redis cache with a TTL func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { - // Note: This is a placeholder implementation - // In a real implementation, we would: - // 1. Serialize the value to JSON - // 2. Store in Redis with the TTL + if c.client == nil { + return ErrNotConnected + } + + data, err := json.Marshal(value) + if err != nil { + return ErrInvalidValue + } + + if err := c.client.Set(ctx, key, data, ttl).Err(); err != nil { + return fmt.Errorf("failed to set Redis key %s: %w", key, err) + } return nil } // Delete removes an item from the Redis cache func (c *RedisCache) Delete(ctx context.Context, key string) error { - // Note: This is a placeholder implementation + if c.client == nil { + return ErrNotConnected + } + + if err := c.client.Del(ctx, key).Err(); err != nil { + return fmt.Errorf("failed to delete Redis key %s: %w", key, err) + } return nil } // Flush removes all items from the Redis cache func (c *RedisCache) Flush(ctx context.Context) error { - // Note: This is a placeholder implementation + if c.client == nil { + return ErrNotConnected + } + + if err := c.client.FlushDB(ctx).Err(); err != nil { + return fmt.Errorf("failed to flush Redis database: %w", err) + } return nil } // GetMulti retrieves multiple items from the Redis cache func (c *RedisCache) GetMulti(ctx context.Context, keys []string) (map[string]interface{}, error) { - // Note: This is a placeholder implementation - return make(map[string]interface{}), nil + if len(keys) == 0 { + return make(map[string]interface{}), nil + } + + if c.client == nil { + return nil, ErrNotConnected + } + + vals, err := c.client.MGet(ctx, keys...).Result() + if err != nil { + return nil, fmt.Errorf("failed to get multiple Redis keys: %w", err) + } + + result := make(map[string]interface{}, len(keys)) + for i, val := range vals { + if val != nil { + var value interface{} + if str, ok := val.(string); ok { + if err := json.Unmarshal([]byte(str), &value); err == nil { + result[keys[i]] = value + } + } + } + } + + return result, nil } // SetMulti stores multiple items in the Redis cache with a TTL func (c *RedisCache) SetMulti(ctx context.Context, items map[string]interface{}, ttl time.Duration) error { - // Note: This is a placeholder implementation + if len(items) == 0 { + return nil + } + + if c.client == nil { + return ErrNotConnected + } + + pipe := c.client.Pipeline() + for key, value := range items { + data, err := json.Marshal(value) + if err != nil { + return ErrInvalidValue + } + pipe.Set(ctx, key, data, ttl) + } + + _, err := pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("failed to execute Redis pipeline for SetMulti: %w", err) + } return nil } // DeleteMulti removes multiple items from the Redis cache func (c *RedisCache) DeleteMulti(ctx context.Context, keys []string) error { - // Note: This is a placeholder implementation + if len(keys) == 0 { + return nil + } + + if c.client == nil { + return ErrNotConnected + } + + if err := c.client.Del(ctx, keys...).Err(); err != nil { + return fmt.Errorf("failed to delete multiple Redis keys: %w", err) + } return nil } - -// Note: The actual Redis implementation would be completed when adding Redis dependency