@@ -2,58 +2,189 @@ package contract
22
33import (
44 "context"
5+ "encoding/json"
56 "errors"
67 "time"
78)
89
910var (
1011 // ErrCacheKeyNotFound is returned when a key does not exist in the cache.
11- // Callers should also supply additional context, such as the cache key.
1212 ErrCacheKeyNotFound = errors .New ("cache key not found" )
1313
14- // ErrCacheUnsupportedOperation is returned when a method such as Forever
15- // or atomic operations is not supported by the cache backend .
14+ // ErrCacheUnsupportedOperation is returned when a method such as
15+ // atomic increment/decrement is not supported by the cache driver .
1616 ErrCacheUnsupportedOperation = errors .New ("cache unsupported operation" )
1717)
1818
19- // Cache defines a generic cache contract inspired by Laravel's Cache Repository.
20- // It supports basic CRUD, atomic operations, and lazy-loading via Remember patterns.
21- type Cache interface {
22- // Get retrieves the value for the given key.
23- // Returns nil and an error if the key is missing or retrieval fails.
24- Get (ctx context.Context , key string ) (any , error )
19+ // CacheDriver defines the minimal contract that cache backends must
20+ // implement. Drivers operate on raw bytes, leaving serialization to
21+ // the [Cache] wrapper. A zero TTL means the entry should never expire.
22+ type CacheDriver interface {
23+ // Get retrieves the raw bytes for the given key.
24+ // Returns [ErrCacheKeyNotFound] when the key is missing or expired.
25+ Get (ctx context.Context , key string ) ([]byte , error )
2526
26- // Put stores a value for the given key with a TTL.
27- // Overwrites any existing value .
28- Put (ctx context.Context , key string , value any , ttl time.Duration ) error
27+ // Put stores raw bytes for the given key with a TTL.
28+ // A zero TTL stores the entry without expiration .
29+ Put (ctx context.Context , key string , value [] byte , ttl time.Duration ) error
2930
30- // Delete removes the cached value for the given key.
31+ // Delete removes the entry for the given key.
3132 // Does nothing if the key does not exist.
3233 Delete (ctx context.Context , key string ) error
3334
34- // Has returns true if the key exists in the cache and is not expired.
35+ // Has returns true if the key exists and is not expired.
3536 Has (ctx context.Context , key string ) (bool , error )
37+ }
38+
39+ // CacheCounter is an optional interface that cache drivers may
40+ // implement to support atomic increment and decrement operations.
41+ // When the driver does not implement this interface, the [Cache]
42+ // wrapper returns [ErrCacheUnsupportedOperation].
43+ type CacheCounter interface {
44+ // Increment atomically increases the integer value at key by
45+ // the given delta. Returns the new value.
46+ Increment (ctx context.Context , key string , delta int64 ) (int64 , error )
47+
48+ // Decrement atomically decreases the integer value at key by
49+ // the given delta. Returns the new value.
50+ Decrement (ctx context.Context , key string , delta int64 ) (int64 , error )
51+ }
52+
53+ // Cache provides a type-safe caching layer over a [CacheDriver].
54+ // It handles JSON serialization and offers convenience methods such
55+ // as Pull, Forever, Remember, and atomic counters. When generic
56+ // methods become available in Go, the Get and Remember methods will
57+ // be updated to return typed values directly.
58+ type Cache struct {
59+ driver CacheDriver
60+ }
61+
62+ // NewCache creates a new [Cache] that delegates storage to the given driver.
63+ func NewCache (driver CacheDriver ) * Cache {
64+ return & Cache {driver : driver }
65+ }
66+
67+ // Driver returns the underlying [CacheDriver].
68+ func (cache * Cache ) Driver () CacheDriver {
69+ return cache .driver
70+ }
71+
72+ // Get retrieves the value for the given key and JSON-decodes it into
73+ // the provided destination. Returns [ErrCacheKeyNotFound] when the
74+ // key is missing.
75+ func (cache * Cache ) Get (ctx context.Context , key string , dest any ) error {
76+ raw , err := cache .driver .Get (ctx , key )
77+
78+ if err != nil {
79+ return err
80+ }
81+
82+ return json .Unmarshal (raw , dest )
83+ }
84+
85+ // Put JSON-encodes the value and stores it with the given TTL.
86+ func (cache * Cache ) Put (ctx context.Context , key string , value any , ttl time.Duration ) error {
87+ raw , err := json .Marshal (value )
88+
89+ if err != nil {
90+ return err
91+ }
92+
93+ return cache .driver .Put (ctx , key , raw , ttl )
94+ }
95+
96+ // Delete removes the cached value for the given key.
97+ func (cache * Cache ) Delete (ctx context.Context , key string ) error {
98+ return cache .driver .Delete (ctx , key )
99+ }
100+
101+ // Has returns true if the key exists in the cache and is not expired.
102+ func (cache * Cache ) Has (ctx context.Context , key string ) (bool , error ) {
103+ return cache .driver .Has (ctx , key )
104+ }
105+
106+ // Pull retrieves and removes the value for the given key, decoding
107+ // it into dest. Returns [ErrCacheKeyNotFound] when the key is missing.
108+ func (cache * Cache ) Pull (ctx context.Context , key string , dest any ) error {
109+ raw , err := cache .driver .Get (ctx , key )
110+
111+ if err != nil {
112+ return err
113+ }
36114
37- // Pull retrieves and removes the value for the given key.
38- // Returns nil and an error if the key is missing.
39- Pull ( ctx context. Context , key string ) ( any , error )
115+ if err := cache . driver . Delete ( ctx , key ); err != nil {
116+ return err
117+ }
40118
41- // Forever stores a value permanently (no TTL).
42- // Only supported if the backend allows non-expiring keys.
43- Forever (ctx context.Context , key string , value any ) error
119+ return json .Unmarshal (raw , dest )
120+ }
121+
122+ // Forever stores a value permanently (zero TTL).
123+ func (cache * Cache ) Forever (ctx context.Context , key string , value any ) error {
124+ return cache .Put (ctx , key , value , 0 )
125+ }
44126
45- // Increment atomically increases the integer value stored at key by the given
46- // amount. Returns the new value or an error if the operation fails.
47- Increment (ctx context.Context , key string , by int64 ) (int64 , error )
127+ // Increment atomically increases the integer value at key by the
128+ // given delta. Returns [ErrCacheUnsupportedOperation] if the driver
129+ // does not implement [CacheCounter].
130+ func (cache * Cache ) Increment (ctx context.Context , key string , delta int64 ) (int64 , error ) {
131+ counter , ok := cache .driver .(CacheCounter )
48132
49- // Decrement atomically decreases the integer value stored at key by
50- // the given amount. Returns the new value or an error if the operation fails.
51- Decrement ( ctx context. Context , key string , by int64 ) ( int64 , error )
133+ if ! ok {
134+ return 0 , ErrCacheUnsupportedOperation
135+ }
52136
53- // Remember attempts to get the value for the given key.
54- // If the key is missing, it calls the compute function, stores the result with the given TTL, and returns it.
55- Remember (ctx context.Context , key string , ttl time.Duration , compute func () (any , error )) (any , error )
137+ return counter .Increment (ctx , key , delta )
138+ }
139+
140+ // Decrement atomically decreases the integer value at key by the
141+ // given delta. Returns [ErrCacheUnsupportedOperation] if the driver
142+ // does not implement [CacheCounter].
143+ func (cache * Cache ) Decrement (ctx context.Context , key string , delta int64 ) (int64 , error ) {
144+ counter , ok := cache .driver .(CacheCounter )
145+
146+ if ! ok {
147+ return 0 , ErrCacheUnsupportedOperation
148+ }
149+
150+ return counter .Decrement (ctx , key , delta )
151+ }
152+
153+ // Remember retrieves the cached value for the given key, decoding it
154+ // into dest. If the key is not found, it calls compute, stores the
155+ // result with the given TTL, and decodes it into dest.
156+ func (cache * Cache ) Remember (ctx context.Context , key string , ttl time.Duration , dest any , compute func () (any , error )) error {
157+ raw , err := cache .driver .Get (ctx , key )
158+
159+ if err == nil {
160+ return json .Unmarshal (raw , dest )
161+ }
162+
163+ if ! errors .Is (err , ErrCacheKeyNotFound ) {
164+ return err
165+ }
166+
167+ value , err := compute ()
168+
169+ if err != nil {
170+ return err
171+ }
172+
173+ encoded , err := json .Marshal (value )
174+
175+ if err != nil {
176+ return err
177+ }
178+
179+ if err := cache .driver .Put (ctx , key , encoded , ttl ); err != nil {
180+ return err
181+ }
182+
183+ return json .Unmarshal (encoded , dest )
184+ }
56185
57- // RememberForever is like Remember but stores the computed result without TTL (permanently).
58- RememberForever (ctx context.Context , key string , compute func () (any , error )) (any , error )
186+ // RememberForever is like [Cache.Remember] but stores the computed
187+ // result without expiration.
188+ func (cache * Cache ) RememberForever (ctx context.Context , key string , dest any , compute func () (any , error )) error {
189+ return cache .Remember (ctx , key , 0 , dest , compute )
59190}
0 commit comments