From 4316ab2197fb281b5e00b1d7e00ebcd171368187 Mon Sep 17 00:00:00 2001 From: Mark Bailey Date: Sun, 31 Aug 2025 19:37:27 -0400 Subject: [PATCH 1/5] feat: add RouteInfo and Router --- http/routing.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 http/routing.go diff --git a/http/routing.go b/http/routing.go new file mode 100644 index 0000000..fd8152c --- /dev/null +++ b/http/routing.go @@ -0,0 +1,36 @@ +package handler + +import ( + "context" + "net/http" + + "github.com/go-chi/chi/v5" +) + +type Router interface { + chi.Router +} + +type RouteInfo struct { + Name string + Path string + Description string + Title string +} + +type routeInfoKey struct{} + +func withRouteName(name, path, description, title string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + info := RouteInfo{ + Name: name, + Path: path, + Description: description, + Title: title, + } + ctx := context.WithValue(r.Context(), routeInfoKey{}, info) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} -- 2.47.2 From c039e8709e160b35c66fa91931f56245562580c1 Mon Sep 17 00:00:00 2001 From: Mark Bailey Date: Sun, 31 Aug 2025 21:22:14 -0400 Subject: [PATCH 2/5] WIP: stopping at no static analysis errors but untested --- handler.go | 21 +++ http/routing.go | 36 ----- interfaces.go | 19 +++ internal/{ => badger}/badger.go | 9 +- internal/{ => badger}/badger_test.go | 2 +- internal/errors/errors.go | 13 ++ internal/route_cache.go | 135 ------------------ middleware/middlware.go | 7 + route_cache.go | 113 +++++++++++++++ ...route_cache_test.go => route_cache_test.go | 2 +- routing.go | 47 ++++++ 11 files changed, 223 insertions(+), 181 deletions(-) create mode 100644 handler.go delete mode 100644 http/routing.go create mode 100644 interfaces.go rename internal/{ => badger}/badger.go (92%) rename internal/{ => badger}/badger_test.go (99%) delete mode 100644 internal/route_cache.go create mode 100644 middleware/middlware.go create mode 100644 route_cache.go rename internal/route_cache_test.go => route_cache_test.go (82%) create mode 100644 routing.go diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..1d6fff0 --- /dev/null +++ b/handler.go @@ -0,0 +1,21 @@ +package joist + +import ( + "fmt" + "net/http" +) + +type HandlerFn func(w http.ResponseWriter, r *http.Request) error + +func (h HandlerFn) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if err := h(w, r); err != nil { + http.Error(w, fmt.Sprintf(`An internal error has occurred: %s`, err), http.StatusInternalServerError) + return + } +} + +func (h HandlerFn) ToHandlerFunc() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } +} diff --git a/http/routing.go b/http/routing.go deleted file mode 100644 index fd8152c..0000000 --- a/http/routing.go +++ /dev/null @@ -1,36 +0,0 @@ -package handler - -import ( - "context" - "net/http" - - "github.com/go-chi/chi/v5" -) - -type Router interface { - chi.Router -} - -type RouteInfo struct { - Name string - Path string - Description string - Title string -} - -type routeInfoKey struct{} - -func withRouteName(name, path, description, title string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - info := RouteInfo{ - Name: name, - Path: path, - Description: description, - Title: title, - } - ctx := context.WithValue(r.Context(), routeInfoKey{}, info) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} diff --git a/interfaces.go b/interfaces.go new file mode 100644 index 0000000..c30d764 --- /dev/null +++ b/interfaces.go @@ -0,0 +1,19 @@ +package joist + +import ( + "context" + "time" +) + +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 RouteCacher interface { + Add(string, string) error + GetPath(string) (string, error) + All() (map[string]string, error) +} diff --git a/internal/badger.go b/internal/badger/badger.go similarity index 92% rename from internal/badger.go rename to internal/badger/badger.go index e694f4c..f987649 100644 --- a/internal/badger.go +++ b/internal/badger/badger.go @@ -1,4 +1,4 @@ -package internal +package badger import ( "context" @@ -12,13 +12,6 @@ import ( 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 { diff --git a/internal/badger_test.go b/internal/badger/badger_test.go similarity index 99% rename from internal/badger_test.go rename to internal/badger/badger_test.go index 331b6e9..123f730 100644 --- a/internal/badger_test.go +++ b/internal/badger/badger_test.go @@ -1,4 +1,4 @@ -package internal +package badger import ( "context" diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 7e331c1..83cb32b 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -36,3 +36,16 @@ func (e ErrBadgerInit) Error() string { func NewErrBadgerInit(baseDir, subDir string) error { return &ErrBadgerInit{baseDir: baseDir, subDir: subDir} } + +type ContextAwareErr struct { + base error + current error +} + +func NewContextAwareErr(base, e error) ContextAwareErr { + return ContextAwareErr{base: base, current: e} +} + +func (e ContextAwareErr) Error() string { + return fmt.Sprintf(`%s: %s`, e.base, e.current) +} diff --git a/internal/route_cache.go b/internal/route_cache.go deleted file mode 100644 index 9524140..0000000 --- a/internal/route_cache.go +++ /dev/null @@ -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 -} diff --git a/middleware/middlware.go b/middleware/middlware.go new file mode 100644 index 0000000..c8f4b9f --- /dev/null +++ b/middleware/middlware.go @@ -0,0 +1,7 @@ +package middleware + +import "net/http" + +type Handler func(w http.ResponseWriter, r *http.Request) error + +type Middleware func(Handler) Handler diff --git a/route_cache.go b/route_cache.go new file mode 100644 index 0000000..ce00ae9 --- /dev/null +++ b/route_cache.go @@ -0,0 +1,113 @@ +package joist + +import ( + "context" + "regexp" + "strings" + "sync" + "time" + + "git.markbailey.dev/cerbervs/joist/internal/badger" + jerr "git.markbailey.dev/cerbervs/joist/internal/errors" +) + +const suff = "route_cache" + +type routeCache struct { + rs map[string]string + s Storer + ttl time.Duration +} + +type RouteCacheOpt func(*routeCache) + +var ( + rcLock sync.Mutex + rcOnce sync.Once + c *routeCache +) + +func WithTTL(ttl time.Duration) RouteCacheOpt { + return func(r *routeCache) { + r.ttl = ttl + } +} + +func WithStore(store Storer) RouteCacheOpt { + return func(r *routeCache) { + r.s = store + } +} + +func NewRouteCache(opts ...RouteCacheOpt) (*routeCache, error) { + s, err := badger.NewBadgerStore(badger.WithSubDir("route_cache")) + if err != nil { + return nil, jerr.NewContextAwareErr(jerr.ErrCacheUninitialized, err) + } + + c = &routeCache{ + rs: make(map[string]string), + s: s, + } + + for _, opt := range opts { + opt(c) + } + + return c, nil +} + +func (r *routeCache) All() (map[string]string, error) { + if r.rs == nil { + return nil, jerr.ErrCacheUninitialized + } + return r.rs, nil +} + +func (r *routeCache) Add(name string, path string) error { + ttl := r.ttl + if ttl == 0 { + ttl = 1 * time.Hour + } + + if err := r.s.Put(context.Background(), name, []byte(path), ttl); err != nil { + return err + } + + return nil +} + +func (r *routeCache) GetPath(name string) (string, error) { + if path, ok := r.rs[name]; ok { + return path, nil + } + + return string(""), jerr.NewErrRouteNotFound(string(name)) +} + +func (r *routeCache) GetDescription(name string) (string, error) { + // TODO: implement + 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 +} diff --git a/internal/route_cache_test.go b/route_cache_test.go similarity index 82% rename from internal/route_cache_test.go rename to route_cache_test.go index fccad65..1b633d2 100644 --- a/internal/route_cache_test.go +++ b/route_cache_test.go @@ -1,4 +1,4 @@ -package internal +package joist import "testing" diff --git a/routing.go b/routing.go new file mode 100644 index 0000000..497e3ec --- /dev/null +++ b/routing.go @@ -0,0 +1,47 @@ +package joist + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/go-chi/chi/v5" +) + +type Router struct { + chi.Mux + cache RouteCacher +} + +type RouteInfo struct { + Name string + Path string + Description string + Title string +} + +type routeInfoKey struct{} + +func NewRouter() Router { + cache, err := NewRouteCache(WithTTL(8 * time.Hour)) + if err != nil { + log.Fatal("failed to create route cache") + } + + return Router{cache: cache} +} + +func WithRouteInfo(ri RouteInfo) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), routeInfoKey{}, ri) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func GetRouteInfo(r *http.Request) (RouteInfo, bool) { + info, ok := r.Context().Value(routeInfoKey{}).(RouteInfo) + return info, ok +} -- 2.47.2 From 50f3748ed2e88c652baf85392cac8805b7b72bc8 Mon Sep 17 00:00:00 2001 From: Mark Bailey Date: Sun, 31 Aug 2025 21:23:05 -0400 Subject: [PATCH 3/5] WIP: note for next session --- routing.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routing.go b/routing.go index 497e3ec..47d7248 100644 --- a/routing.go +++ b/routing.go @@ -32,6 +32,7 @@ func NewRouter() Router { return Router{cache: cache} } +// FIX: figure out the right middleware pattern or way to inject route info func WithRouteInfo(ri RouteInfo) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -- 2.47.2 From 4c6e0c9ffb6673af4ee81682e4d54d9dbb486f81 Mon Sep 17 00:00:00 2001 From: Mark Bailey Date: Sat, 6 Sep 2025 15:20:47 -0400 Subject: [PATCH 4/5] refactor(errors): rename ContextAwareErr to WrappedErr --- internal/errors/errors.go | 34 +++++++++++++++++++--------------- route_cache.go | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 83cb32b..262c91e 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -9,15 +9,15 @@ import ( var ErrCacheUninitialized = errors.New("the route cache is uninitialized") type ErrRouteNotFound struct { - n string + route string +} + +func NewErrRouteNotFound(route string) error { + return &ErrRouteNotFound{route: route} } func (e ErrRouteNotFound) Error() string { - return "route not found: " + string(e.n) -} - -func NewErrRouteNotFound(n string) error { - return &ErrRouteNotFound{n: n} + return "route not found: " + string(e.route) } type ErrBadgerInit struct { @@ -25,6 +25,10 @@ type ErrBadgerInit struct { subDir string } +func NewErrBadgerInit(baseDir, subDir string) error { + return &ErrBadgerInit{baseDir: baseDir, subDir: subDir} +} + func (e ErrBadgerInit) Error() string { if e.subDir == "" { return fmt.Sprintf("failed to initialize badger store at base directory: %s", e.baseDir) @@ -33,19 +37,19 @@ func (e ErrBadgerInit) Error() string { 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} +type WrappedErr struct { + e error + with error } -type ContextAwareErr struct { - base error - current error +func Wrap(e, with error) WrappedErr { + return WrappedErr{e: e, with: with} } -func NewContextAwareErr(base, e error) ContextAwareErr { - return ContextAwareErr{base: base, current: e} +func (e WrappedErr) Error() string { + return fmt.Sprintf(`%s: %s`, e.e, e.with) } -func (e ContextAwareErr) Error() string { - return fmt.Sprintf(`%s: %s`, e.base, e.current) +func (e WrappedErr) Unwrap() error { + return e.e } diff --git a/route_cache.go b/route_cache.go index ce00ae9..7614384 100644 --- a/route_cache.go +++ b/route_cache.go @@ -42,7 +42,7 @@ func WithStore(store Storer) RouteCacheOpt { func NewRouteCache(opts ...RouteCacheOpt) (*routeCache, error) { s, err := badger.NewBadgerStore(badger.WithSubDir("route_cache")) if err != nil { - return nil, jerr.NewContextAwareErr(jerr.ErrCacheUninitialized, err) + return nil, jerr.Wrap(jerr.ErrCacheUninitialized, err) } c = &routeCache{ -- 2.47.2 From a8e8a97e85feba659a19f9c87206d18bb13faff0 Mon Sep 17 00:00:00 2001 From: Mark Bailey Date: Sat, 6 Sep 2025 17:56:59 -0400 Subject: [PATCH 5/5] feat: finish routing --- handler.go | 12 +- interfaces.go | 13 ++- internal/badger/badger.go | 42 +++++++ internal/errors/errors.go | 6 +- middleware/{middlware.go => middleware.go} | 0 route_cache.go | 121 ++++++++++++++------- routing.go | 74 ++++++++++--- 7 files changed, 197 insertions(+), 71 deletions(-) rename middleware/{middlware.go => middleware.go} (100%) diff --git a/handler.go b/handler.go index 1d6fff0..21d13ab 100644 --- a/handler.go +++ b/handler.go @@ -5,17 +5,13 @@ import ( "net/http" ) -type HandlerFn func(w http.ResponseWriter, r *http.Request) error +type ( + HandlerFn func(w http.ResponseWriter, r *http.Request) error + MiddlewareFn func(HandlerFn) HandlerFn +) func (h HandlerFn) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := h(w, r); err != nil { http.Error(w, fmt.Sprintf(`An internal error has occurred: %s`, err), http.StatusInternalServerError) - return - } -} - -func (h HandlerFn) ToHandlerFunc() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - h.ServeHTTP(w, r) } } diff --git a/interfaces.go b/interfaces.go index c30d764..e63123c 100644 --- a/interfaces.go +++ b/interfaces.go @@ -7,13 +7,16 @@ import ( type Storer interface { Close() error - Delete(context.Context, string) error - Get(context.Context, string) ([]byte, error) - Put(context.Context, string, []byte, time.Duration) error + Delete(ctx context.Context, key string) error + Get(ctx context.Context, key string) ([]byte, error) + GetForPrefix(ctx context.Context, prefix string) (map[string][]byte, error) + Put(ctx context.Context, key string, val []byte, ttl time.Duration) error } type RouteCacher interface { - Add(string, string) error - GetPath(string) (string, error) + Add(name string, path string) error + AddRouteInfo(routeInfo RouteInfo) error + GetPath(name string) (string, error) + GetRouteInfo(name string) (RouteInfo, error) All() (map[string]string, error) } diff --git a/internal/badger/badger.go b/internal/badger/badger.go index f987649..9f36eaf 100644 --- a/internal/badger/badger.go +++ b/internal/badger/badger.go @@ -51,6 +51,48 @@ func (b *badgerStore) Put(ctx context.Context, key string, val []byte, ttl time. } } +func (b *badgerStore) GetForPrefix(ctx context.Context, prefix string) (map[string][]byte, error) { + done := make(chan struct { + val map[string][]byte + err error + }, 1) + + go func() { + valCopy := make(map[string][]byte) + err := b.db.View(func(txn *badger.Txn) error { + it := txn.NewIterator(badger.DefaultIteratorOptions) + defer it.Close() + prefix := []byte(prefix) + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + item := it.Item() + k := item.Key() + err := item.Value(func(v []byte) error { + valCopy[string(k)] = v + return nil + }) + if err != nil { + done <- struct { + val map[string][]byte + err error + }{make(map[string][]byte), err} + } + } + return nil + }) + done <- struct { + val map[string][]byte + err error + }{valCopy, err} + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case res := <-done: + return res.val, nil + } +} + func (b *badgerStore) Get(ctx context.Context, key string) ([]byte, error) { done := make(chan struct { val []byte diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 262c91e..e89e786 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -6,7 +6,11 @@ import ( "path/filepath" ) -var ErrCacheUninitialized = errors.New("the route cache is uninitialized") +var ( + ErrCacheUninitialized = errors.New("the route cache is uninitialized") + ErrFailedToCreateRouteCache = errors.New("failed to create route cache") + ErrRouterNotCreated = errors.New("router not created") +) type ErrRouteNotFound struct { route string diff --git a/middleware/middlware.go b/middleware/middleware.go similarity index 100% rename from middleware/middlware.go rename to middleware/middleware.go diff --git a/route_cache.go b/route_cache.go index 7614384..662b55b 100644 --- a/route_cache.go +++ b/route_cache.go @@ -2,8 +2,8 @@ package joist import ( "context" - "regexp" - "strings" + "errors" + "log" "sync" "time" @@ -11,10 +11,9 @@ import ( jerr "git.markbailey.dev/cerbervs/joist/internal/errors" ) -const suff = "route_cache" - type routeCache struct { rs map[string]string + ris map[string]*RouteInfo s Storer ttl time.Duration } @@ -22,9 +21,8 @@ type routeCache struct { type RouteCacheOpt func(*routeCache) var ( - rcLock sync.Mutex rcOnce sync.Once - c *routeCache + cache *routeCache ) func WithTTL(ttl time.Duration) RouteCacheOpt { @@ -39,22 +37,21 @@ func WithStore(store Storer) RouteCacheOpt { } } -func NewRouteCache(opts ...RouteCacheOpt) (*routeCache, error) { - s, err := badger.NewBadgerStore(badger.WithSubDir("route_cache")) - if err != nil { - return nil, jerr.Wrap(jerr.ErrCacheUninitialized, err) +func NewRouteCacheService(opts ...RouteCacheOpt) (*routeCache, error) { + rcOnce.Do(func() { + c, err := newRouteCache(opts...) + if err != nil { + log.Fatal(err) + } + + cache = c + }) + + if cache == nil { + return nil, jerr.ErrCacheUninitialized } - c = &routeCache{ - rs: make(map[string]string), - s: s, - } - - for _, opt := range opts { - opt(c) - } - - return c, nil + return cache, nil } func (r *routeCache) All() (map[string]string, error) { @@ -74,6 +71,8 @@ func (r *routeCache) Add(name string, path string) error { return err } + r.rs[name] = path + return nil } @@ -82,32 +81,70 @@ func (r *routeCache) GetPath(name string) (string, error) { return path, nil } - return string(""), jerr.NewErrRouteNotFound(string(name)) -} - -func (r *routeCache) GetDescription(name string) (string, error) { - // TODO: implement - return "", nil -} - -func convertRouteToRouteName(route string) (string, error) { - name := "app" - if route == "/" { - return name + ".home", nil + if path, err := r.s.Get(context.Background(), name); err == nil { + return string(path), nil } - route = strings.ReplaceAll(route, "/", ".") - route = strings.ReplaceAll(route, "-", "_") + return "", errors.New("route not found in cache") +} - paramRegex := regexp.MustCompile(`\.{[^}]*}`) - params := paramRegex.FindAllString(route, -1) - route = paramRegex.ReplaceAllString(route, "") - - if len(params) > 0 { - route += ".params" +func (r *routeCache) AddRouteInfo(routeInfo RouteInfo) error { + ttl := r.ttl + if ttl == 0 { + ttl = 1 * time.Hour } - name += strings.TrimRight(route, ".") + if err := r.s.Put(context.Background(), routeInfo.Name+":method", []byte(routeInfo.Method), ttl); err != nil { + return err + } + if err := r.s.Put(context.Background(), routeInfo.Name+":path", []byte(routeInfo.Path), ttl); err != nil { + return err + } + if err := r.s.Put(context.Background(), routeInfo.Name+":description", []byte(routeInfo.Description), ttl); err != nil { + return err + } + if err := r.s.Put(context.Background(), routeInfo.Name+":title", []byte(routeInfo.Title), ttl); err != nil { + return err + } - return name, nil + r.rs[routeInfo.Name] = routeInfo.Path + r.ris[routeInfo.Name] = &routeInfo + + return nil +} + +func (r *routeCache) GetRouteInfo(name string) (RouteInfo, error) { + if ri, ok := r.ris[name]; ok { + return *ri, nil + } + + if info, err := r.s.GetForPrefix(context.Background(), name); err == nil { + return RouteInfo{ + Method: string(info[":method"]), + Name: name, + Path: string(info[":path"]), + Description: string(info[":description"]), + Title: string(info[":title"]), + }, nil + } + + return RouteInfo{}, errors.New("route info not found in cache") +} + +func newRouteCache(opts ...RouteCacheOpt) (*routeCache, error) { + s, err := badger.NewBadgerStore(badger.WithSubDir("route_cache")) + if err != nil { + return nil, jerr.Wrap(jerr.ErrFailedToCreateRouteCache, err) + } + + cache := &routeCache{ + rs: make(map[string]string), + s: s, + } + + for _, opt := range opts { + opt(cache) + } + + return cache, nil } diff --git a/routing.go b/routing.go index 47d7248..80887b6 100644 --- a/routing.go +++ b/routing.go @@ -4,45 +4,89 @@ import ( "context" "log" "net/http" + "regexp" + "strings" "time" + jerr "git.markbailey.dev/cerbervs/joist/internal/errors" "github.com/go-chi/chi/v5" ) -type Router struct { - chi.Mux - cache RouteCacher -} - type RouteInfo struct { + Method string Name string Path string Description string Title string } +func NewRouteInfo(method, path, description, title string) (RouteInfo, error) { + name, err := convertRouteToRouteName(path) + if err != nil { + return RouteInfo{}, err + } + + return RouteInfo{ + Method: method, + Name: name, + Path: path, + Description: description, + Title: title, + }, nil +} + type routeInfoKey struct{} +type Router struct { + chi.Mux + cache RouteCacher + cacheTTL time.Duration +} + func NewRouter() Router { - cache, err := NewRouteCache(WithTTL(8 * time.Hour)) + ttl := 8 * time.Hour + + cache, err := NewRouteCacheService(WithTTL(ttl)) if err != nil { - log.Fatal("failed to create route cache") + log.Fatal(jerr.Wrap(err, jerr.ErrRouterNotCreated)) } - return Router{cache: cache} + return Router{cache: cache, cacheTTL: ttl} } -// FIX: figure out the right middleware pattern or way to inject route info -func WithRouteInfo(ri RouteInfo) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), routeInfoKey{}, ri) - next.ServeHTTP(w, r.WithContext(ctx)) - }) +func (ro *Router) WithRouteInfo(ri RouteInfo, h HandlerFn) http.HandlerFunc { + ro.cache.AddRouteInfo(ri) + + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), routeInfoKey{}, ri) + h.ServeHTTP(w, r.WithContext(ctx)) } } +// TODO: move to base handler when it's implemented func GetRouteInfo(r *http.Request) (RouteInfo, bool) { info, ok := r.Context().Value(routeInfoKey{}).(RouteInfo) return info, ok } + +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 +} -- 2.47.2