package services import ( "context" "encoding/json" "fmt" "time" "github.com/redis/go-redis/v9" "github.com/rs/zerolog/log" "github.com/treytartt/mycrib-api/internal/config" ) // CacheService provides Redis caching functionality type CacheService struct { client *redis.Client } var cacheInstance *CacheService // NewCacheService creates a new cache service func NewCacheService(cfg *config.RedisConfig) (*CacheService, error) { opt, err := redis.ParseURL(cfg.URL) if err != nil { return nil, fmt.Errorf("failed to parse Redis URL: %w", err) } if cfg.Password != "" { opt.Password = cfg.Password } if cfg.DB != 0 { opt.DB = cfg.DB } client := redis.NewClient(opt) // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { return nil, fmt.Errorf("failed to connect to Redis: %w", err) } log.Info(). Str("url", cfg.URL). Int("db", opt.DB). Msg("Connected to Redis") cacheInstance = &CacheService{client: client} return cacheInstance, nil } // GetCache returns the cache service instance func GetCache() *CacheService { return cacheInstance } // Client returns the underlying Redis client func (c *CacheService) Client() *redis.Client { return c.client } // Set stores a value with expiration func (c *CacheService) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { data, err := json.Marshal(value) if err != nil { return fmt.Errorf("failed to marshal value: %w", err) } return c.client.Set(ctx, key, data, expiration).Err() } // Get retrieves a value by key func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error { data, err := c.client.Get(ctx, key).Bytes() if err != nil { return err } return json.Unmarshal(data, dest) } // GetString retrieves a string value by key func (c *CacheService) GetString(ctx context.Context, key string) (string, error) { return c.client.Get(ctx, key).Result() } // SetString stores a string value with expiration func (c *CacheService) SetString(ctx context.Context, key string, value string, expiration time.Duration) error { return c.client.Set(ctx, key, value, expiration).Err() } // Delete removes a key func (c *CacheService) Delete(ctx context.Context, keys ...string) error { return c.client.Del(ctx, keys...).Err() } // Exists checks if a key exists func (c *CacheService) Exists(ctx context.Context, keys ...string) (int64, error) { return c.client.Exists(ctx, keys...).Result() } // Close closes the Redis connection func (c *CacheService) Close() error { if c.client != nil { return c.client.Close() } return nil } // Auth token cache helpers const ( AuthTokenPrefix = "auth_token_" TokenCacheTTL = 5 * time.Minute ) // CacheAuthToken caches a user ID for a token func (c *CacheService) CacheAuthToken(ctx context.Context, token string, userID uint) error { key := AuthTokenPrefix + token return c.SetString(ctx, key, fmt.Sprintf("%d", userID), TokenCacheTTL) } // GetCachedAuthToken gets a cached user ID for a token func (c *CacheService) GetCachedAuthToken(ctx context.Context, token string) (uint, error) { key := AuthTokenPrefix + token val, err := c.GetString(ctx, key) if err != nil { return 0, err } var userID uint _, err = fmt.Sscanf(val, "%d", &userID) return userID, err } // InvalidateAuthToken removes a cached token func (c *CacheService) InvalidateAuthToken(ctx context.Context, token string) error { key := AuthTokenPrefix + token return c.Delete(ctx, key) } // Static data cache helpers const ( StaticDataKey = "static_data" StaticDataTTL = 1 * time.Hour ) // CacheStaticData caches static lookup data func (c *CacheService) CacheStaticData(ctx context.Context, data interface{}) error { return c.Set(ctx, StaticDataKey, data, StaticDataTTL) } // GetCachedStaticData retrieves cached static data func (c *CacheService) GetCachedStaticData(ctx context.Context, dest interface{}) error { return c.Get(ctx, StaticDataKey, dest) } // InvalidateStaticData removes cached static data func (c *CacheService) InvalidateStaticData(ctx context.Context) error { return c.Delete(ctx, StaticDataKey) }