Initial commit.

This commit is contained in:
2025-12-10 21:09:25 +00:00
commit 9d4de0e6b6
73 changed files with 15219 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
package cache
import (
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"os"
"path/filepath"
"sync"
"time"
)
// Cache defines the interface for caching
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{})
Delete(key string)
Clear() error
}
// FileCache implements file-based caching
type FileCache struct {
directory string
ttl time.Duration
mu sync.RWMutex
}
// cacheEntry wraps a cached value with expiration
type cacheEntry struct {
Value interface{}
ExpiresAt time.Time
}
// NewFileCache creates a new file-based cache
func NewFileCache(directory string, ttl time.Duration) (*FileCache, error) {
// Create directory if it doesn't exist
if err := os.MkdirAll(directory, 0750); err != nil {
return nil, err
}
return &FileCache{
directory: directory,
ttl: ttl,
}, nil
}
// Get retrieves a value from the cache
func (c *FileCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
path := c.keyToPath(key)
file, err := os.Open(path) // #nosec G304 -- path is internally generated hash
if err != nil {
return nil, false
}
defer file.Close()
var entry cacheEntry
decoder := gob.NewDecoder(file)
if err := decoder.Decode(&entry); err != nil {
return nil, false
}
// Check expiration
if time.Now().After(entry.ExpiresAt) {
_ = os.Remove(path)
return nil, false
}
return entry.Value, true
}
// Set stores a value in the cache
func (c *FileCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
entry := cacheEntry{
Value: value,
ExpiresAt: time.Now().Add(c.ttl),
}
path := c.keyToPath(key)
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
return
}
file, err := os.Create(path) // #nosec G304 -- path is internally generated hash
if err != nil {
return
}
defer file.Close()
encoder := gob.NewEncoder(file)
_ = encoder.Encode(entry)
}
// Delete removes a value from the cache
func (c *FileCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
path := c.keyToPath(key)
_ = os.Remove(path)
}
// Clear removes all cached values
func (c *FileCache) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
return os.RemoveAll(c.directory)
}
// keyToPath converts a cache key to a file path
func (c *FileCache) keyToPath(key string) string {
hash := sha256.Sum256([]byte(key))
filename := hex.EncodeToString(hash[:8]) + ".gob"
return filepath.Join(c.directory, filename)
}
// NoopCache is a cache that doesn't cache anything
type NoopCache struct{}
// NewNoopCache creates a new no-op cache
func NewNoopCache() *NoopCache {
return &NoopCache{}
}
// Get always returns false
func (c *NoopCache) Get(key string) (interface{}, bool) {
return nil, false
}
// Set does nothing
func (c *NoopCache) Set(key string, value interface{}) {}
// Delete does nothing
func (c *NoopCache) Delete(key string) {}
// Clear does nothing
func (c *NoopCache) Clear() error {
return nil
}
// MemoryCache implements in-memory caching (useful for testing)
type MemoryCache struct {
data map[string]cacheEntry
ttl time.Duration
mu sync.RWMutex
}
// NewMemoryCache creates a new in-memory cache
func NewMemoryCache(ttl time.Duration) *MemoryCache {
return &MemoryCache{
data: make(map[string]cacheEntry),
ttl: ttl,
}
}
// Get retrieves a value from the cache
func (c *MemoryCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.data[key]
if !ok {
return nil, false
}
// Check expiration
if time.Now().After(entry.ExpiresAt) {
delete(c.data, key)
return nil, false
}
return entry.Value, true
}
// Set stores a value in the cache
func (c *MemoryCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = cacheEntry{
Value: value,
ExpiresAt: time.Now().Add(c.ttl),
}
}
// Delete removes a value from the cache
func (c *MemoryCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
// Clear removes all cached values
func (c *MemoryCache) Clear() error {
c.mu.Lock()
defer c.mu.Unlock()
c.data = make(map[string]cacheEntry)
return nil
}
// Register types for gob encoding
func init() {
// Register common types that might be cached
gob.Register([]interface{}{})
gob.Register(map[string]interface{}{})
}
+290
View File
@@ -0,0 +1,290 @@
package cache
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFileCache_Basic(t *testing.T) {
// Create temp directory for cache
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
// Test Set and Get
cache.Set("test-key", "test-value")
value, ok := cache.Get("test-key")
assert.True(t, ok)
assert.Equal(t, "test-value", value)
}
func TestFileCache_GetNonExistent(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
value, ok := cache.Get("non-existent")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestFileCache_Expiration(t *testing.T) {
tempDir := t.TempDir()
// Use a very short TTL
cache, err := NewFileCache(tempDir, 50*time.Millisecond)
require.NoError(t, err)
cache.Set("expire-key", "expire-value")
// Should be available immediately
value, ok := cache.Get("expire-key")
assert.True(t, ok)
assert.Equal(t, "expire-value", value)
// Wait for expiration
time.Sleep(100 * time.Millisecond)
// Should be expired now
value, ok = cache.Get("expire-key")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestFileCache_Delete(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
cache.Set("delete-key", "delete-value")
// Verify it exists
_, ok := cache.Get("delete-key")
assert.True(t, ok)
// Delete it
cache.Delete("delete-key")
// Should be gone
value, ok := cache.Get("delete-key")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestFileCache_Clear(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
// Add multiple entries
cache.Set("key1", "value1")
cache.Set("key2", "value2")
cache.Set("key3", "value3")
// Clear the cache
err = cache.Clear()
require.NoError(t, err)
// All should be gone
_, ok := cache.Get("key1")
assert.False(t, ok)
_, ok = cache.Get("key2")
assert.False(t, ok)
_, ok = cache.Get("key3")
assert.False(t, ok)
}
func TestFileCache_ComplexValues(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
// Test with map
mapValue := map[string]interface{}{
"key1": "value1",
"key2": 123,
}
cache.Set("map-key", mapValue)
retrieved, ok := cache.Get("map-key")
assert.True(t, ok)
assert.Equal(t, mapValue, retrieved)
// Test with slice
sliceValue := []interface{}{"a", "b", "c"}
cache.Set("slice-key", sliceValue)
retrieved, ok = cache.Get("slice-key")
assert.True(t, ok)
assert.Equal(t, sliceValue, retrieved)
}
func TestFileCache_CreateDirectory(t *testing.T) {
// Test that NewFileCache creates directory if it doesn't exist
tempDir := filepath.Join(t.TempDir(), "nested", "cache", "dir")
cache, err := NewFileCache(tempDir, time.Hour)
require.NoError(t, err)
// Verify directory was created
info, err := os.Stat(tempDir)
require.NoError(t, err)
assert.True(t, info.IsDir())
// Should be usable
cache.Set("key", "value")
value, ok := cache.Get("key")
assert.True(t, ok)
assert.Equal(t, "value", value)
}
func TestMemoryCache_Basic(t *testing.T) {
t.Parallel()
cache := NewMemoryCache(time.Hour)
// Test Set and Get
cache.Set("test-key", "test-value")
value, ok := cache.Get("test-key")
assert.True(t, ok)
assert.Equal(t, "test-value", value)
}
func TestMemoryCache_GetNonExistent(t *testing.T) {
t.Parallel()
cache := NewMemoryCache(time.Hour)
value, ok := cache.Get("non-existent")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestMemoryCache_Expiration(t *testing.T) {
t.Parallel()
cache := NewMemoryCache(50 * time.Millisecond)
cache.Set("expire-key", "expire-value")
// Should be available immediately
value, ok := cache.Get("expire-key")
assert.True(t, ok)
assert.Equal(t, "expire-value", value)
// Wait for expiration
time.Sleep(100 * time.Millisecond)
// Should be expired now
value, ok = cache.Get("expire-key")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestMemoryCache_Delete(t *testing.T) {
t.Parallel()
cache := NewMemoryCache(time.Hour)
cache.Set("delete-key", "delete-value")
// Verify it exists
_, ok := cache.Get("delete-key")
assert.True(t, ok)
// Delete it
cache.Delete("delete-key")
// Should be gone
value, ok := cache.Get("delete-key")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestMemoryCache_Clear(t *testing.T) {
t.Parallel()
cache := NewMemoryCache(time.Hour)
// Add multiple entries
cache.Set("key1", "value1")
cache.Set("key2", "value2")
cache.Set("key3", "value3")
// Clear the cache
err := cache.Clear()
require.NoError(t, err)
// All should be gone
_, ok := cache.Get("key1")
assert.False(t, ok)
_, ok = cache.Get("key2")
assert.False(t, ok)
_, ok = cache.Get("key3")
assert.False(t, ok)
}
func TestNoopCache_AlwaysReturnsFalse(t *testing.T) {
t.Parallel()
cache := NewNoopCache()
// Set something
cache.Set("key", "value")
// Get should return false
value, ok := cache.Get("key")
assert.False(t, ok)
assert.Nil(t, value)
}
func TestNoopCache_DeleteAndClear(t *testing.T) {
t.Parallel()
cache := NewNoopCache()
// These should not panic or error
cache.Delete("key")
err := cache.Clear()
assert.NoError(t, err)
}
func TestFileCache_KeyToPath(t *testing.T) {
t.Parallel()
cache := &FileCache{directory: "/tmp/cache"}
path1 := cache.keyToPath("key1")
path2 := cache.keyToPath("key2")
path1Again := cache.keyToPath("key1")
// Different keys should produce different paths
assert.NotEqual(t, path1, path2)
// Same key should produce same path
assert.Equal(t, path1, path1Again)
// Path should end with .gob
assert.Contains(t, path1, ".gob")
}
func TestCacheInterface(t *testing.T) {
t.Parallel()
// Ensure all cache types implement the interface
var _ Cache = (*FileCache)(nil)
var _ Cache = (*MemoryCache)(nil)
var _ Cache = (*NoopCache)(nil)
}