feat: add badger store and route cacher

This commit is contained in:
Mark Bailey 2025-08-31 05:54:41 -04:00
parent 983f02edf1
commit 42a063182b
8 changed files with 662 additions and 0 deletions

25
go.mod
View File

@ -1,3 +1,28 @@
module git.markbailey.dev/cerbervs/joist
go 1.24.6
require (
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v4 v4.8.0 // indirect
github.com/dgraph-io/ristretto v0.0.2 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

98
go.sum Normal file
View File

@ -0,0 +1,98 @@
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

139
internal/badger.go Normal file
View File

@ -0,0 +1,139 @@
package internal
import (
"context"
"os"
"path/filepath"
"time"
"git.markbailey.dev/cerbervs/joist/internal/errors"
"github.com/dgraph-io/badger/v4"
)
const defaultBaseDir = "./data/badger"
type BadgerOpts func(*BadgerStore)
func WithBaseDir(baseDir string) BadgerOpts {
return func(b *BadgerStore) {
b.baseDir = baseDir
}
}
func WithSubDir(subDir string) BadgerOpts {
return func(b *BadgerStore) {
b.subDir = subDir
}
}
type BadgerStore struct {
baseDir string
subDir string
db *badger.DB
}
func (b *BadgerStore) Put(ctx context.Context, key string, val []byte, ttl time.Duration) error {
done := make(chan error, 1)
go func() {
err := b.db.Update(func(txn *badger.Txn) error {
e := badger.NewEntry([]byte(key), val).WithTTL(ttl)
return txn.SetEntry(e)
})
done <- err
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
func (b *BadgerStore) Get(ctx context.Context, key string) ([]byte, error) {
done := make(chan struct {
val []byte
err error
}, 1)
go func() {
var valCopy []byte
err := b.db.View(func(txn *badger.Txn) error {
item, err := txn.Get([]byte(key))
if err != nil {
return err
}
valCopy, err = item.ValueCopy(nil)
return err
})
done <- struct {
val []byte
err error
}{valCopy, err}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-done:
return result.val, result.err
}
}
func (b *BadgerStore) Delete(ctx context.Context, key string) error {
done := make(chan error, 1)
go func() {
err := b.db.Update(func(txn *badger.Txn) error {
return txn.Delete([]byte(key))
})
done <- err
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-done:
return err
}
}
func (b *BadgerStore) Close() error {
return b.db.Close()
}
func (b *BadgerStore) dir() string {
if b.baseDir == "" {
b.baseDir = defaultBaseDir
}
if b.subDir == "" {
return b.baseDir
}
return filepath.Join(b.baseDir, b.subDir)
}
func NewBadgerStore(opts ...BadgerOpts) (*BadgerStore, error) {
s := &BadgerStore{}
for _, opt := range opts {
opt(s)
}
fi, err := os.Stat(s.dir())
if err == nil && !fi.IsDir() {
if err := os.MkdirAll(s.dir(), 0755); err != nil {
return nil, errors.NewErrBadgerInit(s.baseDir, s.subDir)
}
}
bopts := badger.DefaultOptions(s.dir())
s.db, err = badger.Open(bopts)
if err != nil {
return nil, err
}
return s, nil
}

187
internal/badger_test.go Normal file
View File

@ -0,0 +1,187 @@
package internal
import (
"context"
"os"
"path/filepath"
"testing"
"time"
)
func newTestStore(t *testing.T) *BadgerStore {
t.Helper()
dir := filepath.Join(os.TempDir(), "badger_test", time.Now().Format("20060102150405"))
store, err := NewBadgerStore(WithBaseDir(dir))
if err != nil {
t.Fatalf("failed to create store: %v", err)
}
t.Cleanup(func() {
store.Close()
os.RemoveAll(dir)
})
return store
}
// generic subtest runner
type subtest struct {
name string
run func(t *testing.T)
}
func runSubtests(t *testing.T, subs []subtest) {
for _, st := range subs {
t.Run(st.name, st.run)
}
}
func TestBadgerStore(t *testing.T) {
tests := []struct {
name string
test func(t *testing.T, store *BadgerStore)
}{
{
name: "Put/Get/Delete roundtrip",
test: func(t *testing.T, store *BadgerStore) {
ctx := context.Background()
key := "foo"
val := []byte("bar")
runSubtests(t, []subtest{
{
name: "Put",
run: func(t *testing.T) {
if err := store.Put(ctx, key, val, time.Minute); err != nil {
t.Fatalf("Put failed: %v", err)
}
},
},
{
name: "Get",
run: func(t *testing.T) {
got, err := store.Get(ctx, key)
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if string(got) != string(val) {
t.Errorf("Get returned %q, want %q", got, val)
}
},
},
{
name: "Delete",
run: func(t *testing.T) {
if err := store.Delete(ctx, key); err != nil {
t.Fatalf("Delete failed: %v", err)
}
},
},
{
name: "Get after delete",
run: func(t *testing.T) {
_, err := store.Get(ctx, key)
if err == nil {
t.Errorf("expected error after delete, got nil")
}
},
},
})
},
},
{
name: "PutWithTTL expires",
test: func(t *testing.T, store *BadgerStore) {
ctx := context.Background()
key := "ttl"
val := []byte("value")
if err := store.Put(ctx, key, val, time.Second); err != nil {
t.Fatalf("Put failed: %v", err)
}
// Should exist immediately
if _, err := store.Get(ctx, key); err != nil {
t.Fatalf("Get failed immediately: %v", err)
}
// Wait for TTL
time.Sleep(2 * time.Second)
_, err := store.Get(ctx, key)
if err == nil {
t.Errorf("expected error after TTL expiry, got nil")
}
},
},
{
name: "ContextCancellation affects operations",
test: func(t *testing.T, store *BadgerStore) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
runSubtests(t, []subtest{
{
name: "Put fails",
run: func(t *testing.T) {
if err := store.Put(ctx, "x", []byte("y"), time.Minute); err == nil {
t.Errorf("expected Put to fail with canceled context")
}
},
},
{
name: "Get fails",
run: func(t *testing.T) {
if _, err := store.Get(ctx, "x"); err == nil {
t.Errorf("expected Get to fail with canceled context")
}
},
},
{
name: "Delete fails",
run: func(t *testing.T) {
if err := store.Delete(ctx, "x"); err == nil {
t.Errorf("expected Delete to fail with canceled context")
}
},
},
})
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := newTestStore(t)
tt.test(t, store)
})
}
}
func TestDirFunction(t *testing.T) {
tests := []struct {
name string
store *BadgerStore
want string
}{
{
name: "default directory",
store: &BadgerStore{},
want: defaultBaseDir,
},
{
name: "with base and sub dir",
store: &BadgerStore{baseDir: "/tmp", subDir: "foo"},
want: filepath.Join("/tmp", "foo"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.store.dir(); got != tt.want {
t.Errorf("expected dir %q, got %q", tt.want, got)
}
})
}
}

38
internal/errors/errors.go Normal file
View File

@ -0,0 +1,38 @@
package errors
import (
"errors"
"fmt"
"path/filepath"
)
var ErrCacheUninitialized = errors.New("the route cache is uninitialized")
type ErrRouteNotFound struct {
n string
}
func (e ErrRouteNotFound) Error() string {
return "route not found: " + string(e.n)
}
func NewErrRouteNotFound(n string) error {
return &ErrRouteNotFound{n: n}
}
type ErrBadgerInit struct {
baseDir string
subDir string
}
func (e ErrBadgerInit) Error() string {
if e.subDir == "" {
return fmt.Sprintf("failed to initialize badger store at base directory: %s", e.baseDir)
}
return fmt.Sprintf("failed to initialize badger store at directory: %s", filepath.Join(e.baseDir, e.subDir))
}
func NewErrBadgerInit(baseDir, subDir string) error {
return &ErrBadgerInit{baseDir: baseDir, subDir: subDir}
}

View File

@ -0,0 +1,101 @@
package errors
import (
"errors"
"path/filepath"
"testing"
)
func TestErrRouteNotFound(t *testing.T) {
tests := []struct {
name string
route string
expected string
}{
{
name: "empty route name",
route: "",
expected: "route not found: ",
},
{
name: "simple route name",
route: "home",
expected: "route not found: home",
},
{
name: "complex route name",
route: "/api/v1/resource",
expected: "route not found: /api/v1/resource",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewErrRouteNotFound(tt.route)
if err.Error() != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, err.Error())
}
// also check that errors.As works
var target *ErrRouteNotFound
if !errors.As(err, &target) {
t.Errorf("expected error to be of type ErrRouteNotFound")
}
})
}
}
func TestErrBadgerInit(t *testing.T) {
tests := []struct {
name string
baseDir string
subDir string
expected string
}{
{
name: "base dir only",
baseDir: "/tmp/data",
subDir: "",
expected: "failed to initialize badger store at base directory: /tmp/data",
},
{
name: "base dir with sub dir",
baseDir: "/tmp/data",
subDir: "auth",
expected: "failed to initialize badger store at directory: " + filepath.Join("/tmp/data", "auth"),
},
{
name: "empty paths",
baseDir: "",
subDir: "",
expected: "failed to initialize badger store at base directory: ",
},
{
name: "empty base dir with sub dir",
baseDir: "",
subDir: "cache",
expected: "failed to initialize badger store at directory: " + filepath.Join("", "cache"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewErrBadgerInit(tt.baseDir, tt.subDir)
if err.Error() != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, err.Error())
}
// also check that errors.As works
var target *ErrBadgerInit
if !errors.As(err, &target) {
t.Errorf("expected error to be of type ErrBadgerInit")
}
})
}
}
func TestErrCacheUninitialized(t *testing.T) {
if ErrCacheUninitialized.Error() != "the route cache is uninitialized" {
t.Errorf("unexpected error message: %q", ErrCacheUninitialized.Error())
}
}

66
internal/route_cache.go Normal file
View File

@ -0,0 +1,66 @@
package internal
import (
"log"
"sync"
"git.markbailey.dev/cerbervs/joist/internal/errors"
)
const suff = "route_cache"
type routeCache struct {
rs map[string]string
s *BadgerStore
}
var (
rcLock sync.Mutex
rcOnce sync.Once
c *routeCache
)
func NewRouteCache() (*routeCache, error) {
rcOnce.Do(func() {
store, err := NewBadgerStore(WithSubDir("route_cache"))
if err != nil {
log.Printf("failed to create badger store for route cache: %v", err)
c = nil
return
}
c = &routeCache{
rs: make(map[string]string),
s: store,
}
})
if c == nil {
return nil, errors.ErrCacheUninitialized
}
rcLock.Lock()
defer rcLock.Unlock()
return c, nil
}
func (r *routeCache) All() (map[string]string, error) {
if r.rs == nil {
return nil, errors.ErrCacheUninitialized
}
return r.rs, nil
}
func (r *routeCache) Store(name string, path string) error {
// TODO: implement
return nil
}
func (r *routeCache) GetPath(name string) (string, error) {
if path, ok := r.rs[name]; ok {
return path, nil
}
return string(""), errors.NewErrRouteNotFound(string(name))
}

8
route_cache.go Normal file
View File

@ -0,0 +1,8 @@
// Package joist implements the main Joist framework.
package joist
type RouteCacher interface {
Store(string, string) error
GetPath(string) (string, error)
All() (map[string]string, error)
}