🔀 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