main
1// Package cache provides file-based TTL caching for API responses.
2package cache
3
4import (
5 "crypto/sha256"
6 "encoding/json"
7 "fmt"
8 "os"
9 "path/filepath"
10 "time"
11)
12
13// Cache provides file-based TTL caching.
14type Cache struct {
15 dir string
16 ttl time.Duration
17}
18
19type entry struct {
20 ExpiresAt time.Time `json:"expires_at"`
21 Data json.RawMessage `json:"data"`
22}
23
24// New creates a cache with the given directory and TTL.
25func New(dir string, ttl time.Duration) *Cache {
26 return &Cache{dir: dir, ttl: ttl}
27}
28
29// Get retrieves a cached value. Returns false if missing or expired.
30func (c *Cache) Get(key string, dest any) bool {
31 path := c.path(key)
32 data, err := os.ReadFile(path)
33 if err != nil {
34 return false
35 }
36
37 var e entry
38 if err := json.Unmarshal(data, &e); err != nil {
39 os.Remove(path)
40 return false
41 }
42
43 if time.Now().After(e.ExpiresAt) {
44 os.Remove(path)
45 return false
46 }
47
48 return json.Unmarshal(e.Data, dest) == nil
49}
50
51// Set stores a value in the cache.
52func (c *Cache) Set(key string, value any) error {
53 data, err := json.Marshal(value)
54 if err != nil {
55 return err
56 }
57
58 e := entry{
59 ExpiresAt: time.Now().Add(c.ttl),
60 Data: data,
61 }
62
63 out, err := json.Marshal(e)
64 if err != nil {
65 return err
66 }
67
68 if err := os.MkdirAll(c.dir, 0755); err != nil {
69 return err
70 }
71
72 return os.WriteFile(c.path(key), out, 0644)
73}
74
75// GetOrFetch retrieves from cache, or calls fetch and caches the result.
76func GetOrFetch[T any](c *Cache, key string, fetch func() (T, error)) (T, error) {
77 var result T
78 if c != nil && c.Get(key, &result) {
79 return result, nil
80 }
81
82 result, err := fetch()
83 if err != nil {
84 return result, err
85 }
86
87 if c != nil {
88 c.Set(key, result)
89 }
90 return result, nil
91}
92
93func (c *Cache) path(key string) string {
94 hash := sha256.Sum256([]byte(key))
95 return filepath.Join(c.dir, fmt.Sprintf("%x.json", hash[:12]))
96}