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