Compare commits
No commits in common. "c770be157eccaee6fc133f583e40935908e8ec16" and "983f02edf1edf78442d097b0f66ece69eef9532c" have entirely different histories.
c770be157e
...
983f02edf1
22
go.mod
22
go.mod
@ -1,25 +1,3 @@
|
|||||||
module git.markbailey.dev/cerbervs/joist
|
module git.markbailey.dev/cerbervs/joist
|
||||||
|
|
||||||
go 1.24.6
|
go 1.24.6
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dgraph-io/badger/v4 v4.8.0
|
|
||||||
github.com/go-chi/chi/v5 v5.2.3
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
|
||||||
github.com/google/flatbuffers v25.2.10+incompatible // indirect
|
|
||||||
github.com/klauspost/compress v1.18.0 // 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
|
|
||||||
)
|
|
||||||
|
45
go.sum
45
go.sum
@ -1,45 +0,0 @@
|
|||||||
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/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/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/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-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
|
||||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
|
||||||
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/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/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
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/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=
|
|
||||||
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/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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -1,146 +0,0 @@
|
|||||||
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 Storer interface {
|
|
||||||
Close() error
|
|
||||||
Delete(context.Context, string) error
|
|
||||||
Get(context.Context, string) ([]byte, error)
|
|
||||||
Put(context.Context, string, []byte, time.Duration) error
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
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}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ee "git.markbailey.dev/cerbervs/joist/internal/errors"
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
const suff = "route_cache"
|
|
||||||
|
|
||||||
type RouteCacher interface {
|
|
||||||
Store(string, string) error
|
|
||||||
GetPath(string) (string, error)
|
|
||||||
All() (map[string]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeCache struct {
|
|
||||||
rx chi.Router
|
|
||||||
rs map[string]string
|
|
||||||
s Storer
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
rcLock sync.Mutex
|
|
||||||
rcOnce sync.Once
|
|
||||||
c *routeCache
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewRouteCache(store Storer, router chi.Router) (*routeCache, error) {
|
|
||||||
rcOnce.Do(func() {
|
|
||||||
if store == nil {
|
|
||||||
st, err := NewBadgerStore(WithSubDir("route_cache"))
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
store = st
|
|
||||||
}
|
|
||||||
|
|
||||||
c = &routeCache{
|
|
||||||
rx: router,
|
|
||||||
rs: make(map[string]string),
|
|
||||||
s: store,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if c == nil {
|
|
||||||
return nil, ee.ErrCacheUninitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
rcLock.Lock()
|
|
||||||
defer rcLock.Unlock()
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *routeCache) All() (map[string]string, error) {
|
|
||||||
if r.rs == nil {
|
|
||||||
return nil, ee.ErrCacheUninitialized
|
|
||||||
}
|
|
||||||
return r.rs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *routeCache) Store(name string, path string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *routeCache) GetPath(name string) (string, error) {
|
|
||||||
if path, ok := r.rs[name]; ok {
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(""), ee.NewErrRouteNotFound(string(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *routeCache) UnmarshallFromStore() error {
|
|
||||||
if err := chi.Walk(r.rx, walker(r.s)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func walker(store Storer) chi.WalkFunc {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
|
||||||
routeName, err := convertRouteToRouteName(route)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(`failed to name route "%s"`, route)
|
|
||||||
}
|
|
||||||
|
|
||||||
if routeName[len(routeName)-2:] != ".*" {
|
|
||||||
route = regexp.MustCompile(`:.*}`).ReplaceAllString(route, "}")
|
|
||||||
if len(route) > 1 {
|
|
||||||
route = strings.TrimRight(route, "/")
|
|
||||||
}
|
|
||||||
store.Put(ctx, routeName, []byte(route), 1*time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertRouteToRouteName(route string) (string, error) {
|
|
||||||
name := "app"
|
|
||||||
if route == "/" {
|
|
||||||
return name + ".home", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
route = strings.ReplaceAll(route, "/", ".")
|
|
||||||
route = strings.ReplaceAll(route, "-", "_")
|
|
||||||
|
|
||||||
paramRegex := regexp.MustCompile(`\.{[^}]*}`)
|
|
||||||
params := paramRegex.FindAllString(route, -1)
|
|
||||||
route = paramRegex.ReplaceAllString(route, "")
|
|
||||||
|
|
||||||
if len(params) > 0 {
|
|
||||||
route += ".params"
|
|
||||||
}
|
|
||||||
|
|
||||||
name += strings.TrimRight(route, ".")
|
|
||||||
|
|
||||||
return name, nil
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestNewRouteCache(t *testing.T) {
|
|
||||||
// TODO: implement
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user