Skip to content

Commit da5a95a

Browse files
committed
feat(contract): rename interfaces to *Driver, add wrapper structs
Rename all SPI interfaces to *Driver suffix (CacheDriver, DatabaseDriver, EventDriver, SessionDriver) and introduce wrapper structs that take the "good" names (Cache, Database, Events, Session). - CacheDriver/EventDriver operate on []byte; wrappers handle JSON - Session moved from interface to struct with NewSession/NewSessionFrom - Wrapper structs expose Driver() accessor (not embedding) - CacheCounter optional interface for atomic incr/decr - Regenerate mocks for new interface signatures - Update all framework implementations and tests BREAKING CHANGE: All contract interfaces renamed with Driver suffix. Session is now a struct (*contract.Session) instead of an interface. Cache/Events wrappers handle serialization, drivers use raw []byte. This prepares for Go 1.27 generic methods on structs.
1 parent ac652dd commit da5a95a

29 files changed

Lines changed: 1625 additions & 2586 deletions

contract/cache.go

Lines changed: 162 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,189 @@ package contract
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"time"
78
)
89

910
var (
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
}

contract/database.go

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,108 @@ var (
1515
ErrDatabaseNestedTransaction = errors.New("nested transactions are not supported")
1616
)
1717

18-
// Database defines a generic interface for interacting with a SQL-based datastore.
19-
// It provides methods for executing queries, retrieving multiple or single records,
20-
// and performing operations within a transaction context.
21-
type Database interface {
22-
// Close closes the database connection and releases any associated resources.
23-
// It should be called when the database is no longer needed to prevent
24-
// resource leaks. After calling Close, the Database instance should not
25-
// be used for further operations.
18+
// DatabaseDriver defines the interface for interacting with a SQL-based
19+
// datastore. Implementations handle query execution, scanning, and
20+
// transaction management. The [Database] wrapper provides type-safe
21+
// convenience methods on top of this driver.
22+
type DatabaseDriver interface {
23+
// Close closes the database connection and releases associated resources.
2624
Close() error
2725

28-
// Ping verifies that the database connection is still alive and accessible.
29-
// It sends a simple query to the database to test connectivity and returns
30-
// an error if the connection is unavailable or the database is unreachable.
31-
// This method is typically used for health checks and connection validation.
32-
// Ping will also connect to the database if the connection was not established.
26+
// Ping verifies that the database connection is still alive.
3327
Ping(ctx context.Context) error
3428

3529
// Exec executes a SQL query that modifies data (e.g., INSERT, UPDATE, DELETE).
36-
// It returns the number of rows affected.
30+
// Returns the number of rows affected.
3731
Exec(ctx context.Context, query string, args ...any) (int64, error)
3832

39-
// ExecNamed executes a SQL query that modifies data using named parameters.
33+
// ExecNamed executes a SQL query using named parameters.
4034
// The arg parameter should be a struct or map containing the named parameters.
4135
// Returns the number of rows affected.
4236
ExecNamed(ctx context.Context, query string, arg any) (int64, error)
4337

4438
// Select executes a query and scans the results into dest.
4539
// Dest should be a pointer to a slice of structs (e.g., *[]User).
46-
// Returns an error if the query fails or if scanning is unsuccessful.
4740
Select(ctx context.Context, query string, dest any, args ...any) error
4841

49-
// SelectNamed executes a query using named parameters and scans the results into dest.
50-
// Dest should be a pointer to a slice of structs, and arg should contain the named parameters.
51-
// Returns an error if the query fails or if scanning is unsuccessful.
42+
// SelectNamed executes a query using named parameters and scans results into dest.
43+
// Dest should be a pointer to a slice of structs.
5244
SelectNamed(ctx context.Context, query string, dest any, arg any) error
5345

54-
// Find executes a query expected to return a single row,
55-
// and scans the result into dest. Dest should be a pointer to a struct.
56-
// Returns an error if no row is found, multiple rows are returned, or scanning fails.
46+
// Find executes a query expected to return a single row and scans
47+
// the result into dest. Dest should be a pointer to a struct.
5748
Find(ctx context.Context, query string, dest any, args ...any) error
5849

59-
// FindNamed executes a query using named parameters expected to return a single row,
60-
// and scans the result into dest. Dest should be a pointer to a struct,
61-
// and arg should contain the named parameters.
62-
// Returns an error if no row is found, multiple rows are returned, or scanning fails.
50+
// FindNamed executes a query using named parameters expected to return
51+
// a single row and scans the result into dest.
6352
FindNamed(ctx context.Context, query string, dest any, arg any) error
6453

65-
// WithTransaction executes the provided function fn within a database transaction.
66-
// If fn returns an error, the transaction is rolled back. Otherwise, it is committed.
67-
// The tx passed to fn implements the same Database interface and can be used
68-
// for operations within the transaction scope.
69-
WithTransaction(ctx context.Context, fn func(tx Database) error) error
54+
// WithTransaction executes fn within a database transaction. If fn returns
55+
// an error, the transaction is rolled back. Otherwise, it is committed.
56+
WithTransaction(ctx context.Context, fn func(tx DatabaseDriver) error) error
57+
}
58+
59+
// Database provides a wrapper over a [DatabaseDriver] with convenience
60+
// methods. When generic methods become available in Go, Find and Select
61+
// will be updated to return typed values directly.
62+
type Database struct {
63+
driver DatabaseDriver
64+
}
65+
66+
// NewDatabase creates a new [Database] that delegates operations to the given driver.
67+
func NewDatabase(driver DatabaseDriver) *Database {
68+
return &Database{driver: driver}
69+
}
70+
71+
// Driver returns the underlying [DatabaseDriver].
72+
func (database *Database) Driver() DatabaseDriver {
73+
return database.driver
74+
}
75+
76+
// Close closes the database connection and releases associated resources.
77+
func (database *Database) Close() error {
78+
return database.driver.Close()
79+
}
80+
81+
// Ping verifies that the database connection is still alive.
82+
func (database *Database) Ping(ctx context.Context) error {
83+
return database.driver.Ping(ctx)
84+
}
85+
86+
// Exec executes a SQL query that modifies data. Returns the number of rows affected.
87+
func (database *Database) Exec(ctx context.Context, query string, args ...any) (int64, error) {
88+
return database.driver.Exec(ctx, query, args...)
89+
}
90+
91+
// ExecNamed executes a SQL query using named parameters. Returns the number of rows affected.
92+
func (database *Database) ExecNamed(ctx context.Context, query string, arg any) (int64, error) {
93+
return database.driver.ExecNamed(ctx, query, arg)
94+
}
95+
96+
// Select executes a query and scans the results into dest.
97+
// Dest should be a pointer to a slice of structs.
98+
func (database *Database) Select(ctx context.Context, query string, dest any, args ...any) error {
99+
return database.driver.Select(ctx, query, dest, args...)
100+
}
101+
102+
// SelectNamed executes a query using named parameters and scans results into dest.
103+
func (database *Database) SelectNamed(ctx context.Context, query string, dest any, arg any) error {
104+
return database.driver.SelectNamed(ctx, query, dest, arg)
105+
}
106+
107+
// Find executes a query expected to return a single row and scans the result into dest.
108+
func (database *Database) Find(ctx context.Context, query string, dest any, args ...any) error {
109+
return database.driver.Find(ctx, query, dest, args...)
110+
}
111+
112+
// FindNamed executes a query using named parameters expected to return a single row.
113+
func (database *Database) FindNamed(ctx context.Context, query string, dest any, arg any) error {
114+
return database.driver.FindNamed(ctx, query, dest, arg)
115+
}
116+
117+
// WithTransaction executes fn within a database transaction.
118+
func (database *Database) WithTransaction(ctx context.Context, fn func(tx *Database) error) error {
119+
return database.driver.WithTransaction(ctx, func(tx DatabaseDriver) error {
120+
return fn(NewDatabase(tx))
121+
})
70122
}

0 commit comments

Comments
 (0)