🔀 Merge branch 'r/0.5.0' into stable

This commit is contained in:
Dan Jones 2025-04-21 12:18:59 -05:00
commit 5cd8defccd
10 changed files with 364 additions and 0 deletions

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Dependency directories
vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
build/
.task/

39
.golangci.yaml Normal file
View file

@ -0,0 +1,39 @@
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- copyloopvar
- dupl
- err113
- errname
- exptostd
- fatcontext
- funlen
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- godox
- gosec
- perfsprint
- testifylint
linters-settings:
testifylint:
enable-all: true
disable:
- require-error
gocognit:
min-complexity: 5
gocyclo:
min-complexity: 5
gocritic:
enable-all: true
settings:
hugeParam:
sizeThreshold: 255

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
# Changelog
### [0.5.0] - 2025-04-21 - 🚀 Stable release!
Everything works.

21
README.md Normal file
View file

@ -0,0 +1,21 @@
# ezcache
ezcache is a simple in-memory cache for golang that has data expiry.
## Usage
```go
import "codeberg.org/danjones000/ezcache"
// ...
// Create a user cache which will cache users for five minutes
userCache := ezcache.New(func(id uint64) (User, error) {
// logic to fetch user from database
return User{}, nil
}, 5 * time.Minute)
user, err := user.Get(userID)
// Next time you do user.Get with the same userID within five minutes, it will be fetched from cache.
// After five minutes, it will fetch from the database again.
```

62
Taskfile.yml Normal file
View file

@ -0,0 +1,62 @@
version: '3'
tasks:
default:
cmds:
- task: fmt
- task: test
- task: lint
fmt:
desc: Format go code
sources:
- '**/*.go'
cmds:
- go fmt ./...
- go mod tidy
gen:
desc: Generate files
sources:
- '**/*.go'
cmds:
- go generate ./...
lint:
desc: Do static analysis
sources:
- '**/*.go'
cmds:
- golangci-lint run
test:
desc: Run unit tests
deps: [fmt]
sources:
- '**/*.go'
generates:
- build/cover.out
cmds:
- go test -race -cover -coverprofile build/cover.out ./...
coverage-report:
desc: Build coverage report
deps: [test]
sources:
- build/cover.out
generates:
- build/cover.html
cmds:
- go tool cover -html=build/cover.out -o build/cover.html
serve-report:
desc: Serve the coverage report
deps: [coverage-report]
cmds:
- ip addr list | grep inet
- php -S 0.0.0.0:3434 -t build
serve-docs:
desc: Serve the current docs
cmds:
- godoc -http=0.0.0.0:3434 -play

31
cache.go Normal file
View file

@ -0,0 +1,31 @@
package ezcache
import (
"errors"
"time"
)
// ErrInvalidFetcher is returned by Cache.SetFetcher if the fetcher is invalid.
// This is probably only going to happen if it's nil.
var ErrInvalidFetcher = errors.New("invalid fetcher")
// ErrInvalidExpiry is returned by Cache.SetExpiry if the duration is invalid.
// This is usually if it is <= 0.
var ErrInvalidExpiry = errors.New("invalid duration")
type Fetcher[K comparable, V any] func(K) (V, error)
// Cache represents a Cache for values.
type Cache[K comparable, V any] interface {
// Get will fetch the value for key. If in the cache, it will fetch the cached value.
// If not in the cache, it will use the Fetcher.
// It may return [ErrInvalidFetcher], but if an error is returned, it's probably returned by the
// [Fetcher] itself.
Get(key K) (V, error)
// SetFetcher sets the fetcher for this [Cache].
SetFetcher(f Fetcher[K, V]) error
// SetExpiry sets the expiry for this [Cache].
SetExpiry(d time.Duration) error
}

76
ezcache.go Normal file
View file

@ -0,0 +1,76 @@
package ezcache
import (
"sync"
"time"
)
type ezc[K comparable, V any] struct {
fetch Fetcher[K, V]
exp time.Duration
cache map[K]V
setTime map[K]time.Time
lock sync.RWMutex
timeLock sync.RWMutex
}
func New[K comparable, V any](fetcher Fetcher[K, V], exp time.Duration) (Cache[K, V], error) {
c := &ezc[K, V]{}
err := c.SetFetcher(fetcher)
if err != nil {
return nil, err
}
err = c.SetExpiry(exp)
if err != nil {
return nil, err
}
c.cache = make(map[K]V)
c.setTime = make(map[K]time.Time)
return c, nil
}
func (c *ezc[K, V]) Get(key K) (V, error) {
c.timeLock.RLock()
setTime, ok := c.setTime[key]
c.timeLock.RUnlock()
if ok && time.Since(setTime) <= c.exp {
c.lock.RLock()
val, ok := c.cache[key]
c.lock.RUnlock()
if ok {
return val, nil
}
}
val, err := c.fetch(key)
if err != nil {
return val, err
}
c.lock.Lock()
defer c.lock.Unlock()
c.cache[key] = val
c.timeLock.Lock()
defer c.timeLock.Unlock()
c.setTime[key] = time.Now()
return val, nil
}
func (c *ezc[K, V]) SetFetcher(f Fetcher[K, V]) error {
if f == nil {
return ErrInvalidFetcher
}
c.fetch = f
return nil
}
func (c *ezc[K, V]) SetExpiry(exp time.Duration) error {
if exp <= 0 {
return ErrInvalidExpiry
}
c.exp = exp
return nil
}

84
ezcache_test.go Normal file
View file

@ -0,0 +1,84 @@
package ezcache_test
import (
"fmt"
"testing"
"time"
"codeberg.org/danjones000/ezcache"
"github.com/stretchr/testify/assert"
)
var fetcher ezcache.Fetcher[uint8, string] = func(key uint8) (string, error) { return fmt.Sprintf("%d", key), nil }
func TestNewHappy(t *testing.T) {
cache, err := ezcache.New(fetcher, 1)
assert.NoError(t, err)
assert.NotNil(t, cache)
}
func TestNewNilFetcher(t *testing.T) {
cache, err := ezcache.New[uint8, string](nil, 1)
assert.ErrorIs(t, err, ezcache.ErrInvalidFetcher)
assert.Nil(t, cache)
}
func TestNewBadExpiry(tt *testing.T) {
testcases := []struct {
name string
exp time.Duration
}{
{"zero", 0},
{"negative", -5},
}
for _, tc := range testcases {
tt.Run(tc.name, func(t *testing.T) {
cache, err := ezcache.New(fetcher, tc.exp)
assert.ErrorIs(t, err, ezcache.ErrInvalidExpiry)
assert.Nil(t, cache)
})
}
}
func TestGetHappy(t *testing.T) {
var hit bool
cache, _ := ezcache.New(func(key uint8) (string, error) { hit = true; return fetcher(key) }, 5*time.Second)
val, err := cache.Get(4)
assert.NoError(t, err)
assert.Equal(t, "4", val)
assert.True(t, hit)
hit = false
val, err = cache.Get(4)
assert.NoError(t, err)
assert.Equal(t, "4", val)
assert.False(t, hit)
}
func TestGetExpire(t *testing.T) {
var hit bool
cache, _ := ezcache.New(func(key uint8) (string, error) { hit = true; return fetcher(key) }, 1)
val, err := cache.Get(4)
assert.NoError(t, err)
assert.Equal(t, "4", val)
assert.True(t, hit)
hit = false
time.Sleep(2)
val, err = cache.Get(4)
assert.NoError(t, err)
assert.Equal(t, "4", val)
assert.True(t, hit)
}
func TestGetError(t *testing.T) {
cache, _ := ezcache.New(func(k uint8) (byte, error) { return 0, fmt.Errorf("Nope for %d", k) }, 1)
_, err := cache.Get(4)
assert.ErrorContains(t, err, "Nope for 4")
}

11
go.mod Normal file
View file

@ -0,0 +1,11 @@
module codeberg.org/danjones000/ezcache
go 1.23.7
require github.com/stretchr/testify v1.10.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View file

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=