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 +}