🔀 Merge branch 'r/0.5.0' into stable
This commit is contained in:
commit
5cd8defccd
10 changed files with 364 additions and 0 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal 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
39
.golangci.yaml
Normal 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
5
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
### [0.5.0] - 2025-04-21 - 🚀 Stable release!
|
||||||
|
|
||||||
|
Everything works.
|
||||||
21
README.md
Normal file
21
README.md
Normal 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
62
Taskfile.yml
Normal 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
31
cache.go
Normal 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
76
ezcache.go
Normal 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
84
ezcache_test.go
Normal 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
11
go.mod
Normal 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
10
go.sum
Normal 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=
|
||||||
Loading…
Add table
Add a link
Reference in a new issue