Files
inventory-plus-plus/internal/server/router/mux.go
T
2026-01-12 23:21:52 -07:00

660 lines
17 KiB
Go

package router
import (
"fmt"
"net/http"
"path"
"slices"
"strings"
"ruben/inventory2/internal/consts"
"ruben/inventory2/internal/logging"
"ruben/inventory2/internal/server/response"
)
type (
Mux struct {
Mux *http.ServeMux
middleware []response.Middleware
log *logging.Logger
}
)
var (
ErrHandlerNotFound = fmt.Errorf("%w: handler not found", consts.ErrNotFound)
)
func NewMux(log *logging.Logger, ms ...response.Middleware) *Mux {
return &Mux{
Mux: http.NewServeMux(),
middleware: ms,
log: log,
}
}
func (m *Mux) AddMiddleware(ms ...response.Middleware) *Mux {
m.middleware = append(m.middleware, ms...)
return m
}
func (m *Mux) Handle(pattern string, fn response.HandlerFunc) {
log := m.log.With(
"method", "Handle",
"patter", pattern,
"fn", fn,
)
defer log.DebugCallf("called")()
m.Mux.Handle(pattern, response.Handler(m.applyMiddleware(fn)))
}
func (m *Mux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc {
return applyMiddleware(fn, m.middleware...)
}
func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m.Mux.ServeHTTP(w, r)
}
// TODO: support for methods?
// TODO: strip the prefix from the call to the subrouter handler call
// Route does not accept methods or ... wildcards
// func (m *Mux) Route(basePathPattern string, sr Subrouter) {
func (m *Mux) Route(pattern string, sr Subrouter) {
log := m.log.With("method", "Route", "pattern", pattern, "sr", sr)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", nil
})
// ---
method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern)
if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') {
panic("invalid subpath: " + pattern)
}
numPatternSegments := len(segments)
handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr)))
cleanPattern := "/"
if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
if method != "" {
cleanPattern = method + " " + cleanPattern
}
log.Debugf("cleanPattern: %s", cleanPattern)
// ---
/*
trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
if trimmedSubpathPattern == "" && basePathPattern != "/" {
panic("invalid subpath: " + basePathPattern)
}
var trimmedPatternSegments []string
if trimmedSubpathPattern != "" {
trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
}
numPatternSegments := len(trimmedPatternSegments)
handler := response.Handler(m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr)))
cleanPattern := "/"
if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
fmt.Println("Mux.Route: cleanPattern:", cleanPattern)
*/
if method != "" {
m.Mux.Handle(method+" "+cleanPattern, handler)
} else {
m.Mux.Handle("GET "+cleanPattern, handler)
m.Mux.Handle("POST "+cleanPattern, handler)
m.Mux.Handle("PUT "+cleanPattern, handler)
m.Mux.Handle("PATCH "+cleanPattern, handler)
m.Mux.Handle("DELETE "+cleanPattern, handler)
}
}
type (
Subrouter interface {
Handler(r *http.Request) (fn response.HandlerFunc, pathParams map[string]string, found bool)
}
SubMux struct {
tree *muxTree
middleware []response.Middleware
log *logging.Logger
}
)
func NewSubMux(log *logging.Logger, ms ...response.Middleware) *SubMux {
return &SubMux{
tree: newMuxTree(log.WithGroup("muxTree")),
middleware: ms,
log: log,
}
}
// TODO: this is capturing all subroutes!
func (m *SubMux) Handle(pattern string, fn response.HandlerFunc) {
defer m.log.With("pattern", pattern, "fn", fn).DebugCallf("Handle")()
m.tree.set(pattern, m.applyMiddleware(fn))
}
// TODO: need http method support
// Route does not accept methods or ... wildcards
// func (m *SubMux) Route(basePathPattern string, sr Subrouter) {
func (m *SubMux) Route(pattern string, sr Subrouter) {
log := m.log.With("method", "Route", "pattern", pattern, "sr", sr)
defer log.DebugCallf("Route")()
method, segments, _ := getHTTPMethodAndPathSegments(m.log, pattern)
if pattern == "" || (len(segments) == 0 && pattern[len(pattern)-1] != '/') {
panic("invalid subpath: " + pattern)
}
numPatternSegments := len(segments)
handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr))
cleanPattern := "/"
if joinedSegments := strings.Join(segments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
cleanPattern = path.Clean(cleanPattern)
if method != "" {
cleanPattern = method + " " + cleanPattern
}
log.Debugf("Handle about to be called: cleanPattern: %s", cleanPattern)
// ---
/*
// ---
trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
if trimmedSubpathPattern == "" && basePathPattern != "/" {
panic("invalid subpath: " + basePathPattern)
}
var trimmedPatternSegments []string
if trimmedSubpathPattern != "" {
trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
}
numPatternSegments := len(trimmedPatternSegments)
handler := m.applyMiddleware(buildSubrouterHandlerFunc(m.log, numPatternSegments, sr))
// TODO: we're wrapping the pattern method!
cleanPattern := "/"
if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
cleanPattern = "/" + joinedSegments + "/"
}
// ---
*/
m.Handle(cleanPattern, handler)
/*
m.Mux.Handle("GET "+cleanPattern, handler)
m.Mux.Handle("POST "+cleanPattern, handler)
m.Mux.Handle("PUT "+cleanPattern, handler)
m.Mux.Handle("PATCH "+cleanPattern, handler)
m.Mux.Handle("DELETE "+cleanPattern, handler)
*/
}
// // Route does not accept methods or ... wildcards
// func (m *SubMux) Route(basePathPattern string, sr Subrouter) {
// trimmedSubpathPattern := strings.Trim(path.Clean(basePathPattern), "/")
// if trimmedSubpathPattern == "" && basePathPattern != "/" {
// panic("invalid subpath: " + basePathPattern)
// }
//
// var trimmedPatternSegments []string
// if trimmedSubpathPattern != "" {
// trimmedPatternSegments = strings.Split(trimmedSubpathPattern, "/")
// }
// numPatternSegments := len(trimmedPatternSegments)
//
// handler := m.applyMiddleware(buildSubrouterHandlerFunc(numPatternSegments, sr))
//
// // TODO: we're wrapping the pattern method!
// cleanPattern := "/"
// if joinedSegments := strings.Join(trimmedPatternSegments, "/"); joinedSegments != "" {
// cleanPattern = "/" + joinedSegments + "/"
// }
//
// m.Handle(cleanPattern, handler)
//
// /*
// m.Mux.Handle("GET "+cleanPattern, handler)
// m.Mux.Handle("POST "+cleanPattern, handler)
// m.Mux.Handle("PUT "+cleanPattern, handler)
// m.Mux.Handle("PATCH "+cleanPattern, handler)
// m.Mux.Handle("DELETE "+cleanPattern, handler)
// */
// }
func buildSubrouterHandlerFunc(logger *logging.Logger, numPatternSegments int, sr Subrouter) response.HandlerFunc {
log := logger.With(
"function", "buildSubrouterHandlerFunc",
"numPatternSegments", numPatternSegments,
)
return func(r *http.Request) (res response.Response, err error) {
log := log.With("r.URL", r.URL)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"res", res,
"err", err,
}
})
fullPath := r.URL.Path
// set trailing path on request
segments, trailingSlash := getPathSegments(logger, r.URL.Path)
/*
endsInSlash := strings.HasSuffix(r.URL.Path, "/")
segments := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
*/
tail := segments[numPatternSegments:]
tailStr := "/"
if len(tail) > 0 {
tailStr += strings.Join(tail, "/")
if trailingSlash {
tailStr += "/"
}
}
r.URL.Path = tailStr
defer func() {
// reset the request path
r.URL.Path = fullPath
}()
// do the request
log.Debugf("handler called: %s", r.URL)
fn, params, ok := sr.Handler(r)
log.Debugf("handler returned: url = %s, fn = %v, params = %v, ok = %v", r.URL, fn, params, ok)
if !ok {
return nil, response.NotFound().Wrap(ErrHandlerNotFound)
}
for k, v := range params {
r.SetPathValue(k, v)
}
res, err = fn(r)
return res, err
}
}
func (m *SubMux) Handler(r *http.Request) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "Handler",
"r.URL", r.URL,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
return m.tree.get(r.Method, r.URL.Path)
}
func (m *SubMux) applyMiddleware(fn response.HandlerFunc) response.HandlerFunc {
return applyMiddleware(fn, m.middleware...)
}
// path pattern matching tree implementation
type (
muxTree struct {
log *logging.Logger
branches map[string]*muxTree
wildcardKey string
wildcardBranch *muxTree
method string
handler response.HandlerFunc
subrouteHandler response.HandlerFunc
}
)
func newMuxTree(log *logging.Logger) *muxTree {
return &muxTree{
log: log,
branches: make(map[string]*muxTree),
}
}
func (m *muxTree) String() string {
if m == nil {
return "<nil>"
}
kvs := make([]string, 0, 6)
if m.subrouteHandler != nil {
kvs = append(kvs, fmt.Sprintf("subrouteHandler: %v", m.subrouteHandler))
}
if m.method != "" {
kvs = append(kvs, fmt.Sprintf("method: %v", m.method))
}
if m.handler != nil {
kvs = append(kvs, fmt.Sprintf("handler: %v", m.handler))
}
if m.wildcardKey != "" {
kvs = append(kvs, fmt.Sprintf("wildcardKey: %v", m.wildcardKey))
}
if m.wildcardBranch != nil {
kvs = append(kvs, fmt.Sprintf("wildcardBranch: %v", m.wildcardBranch))
}
if len(m.branches) > 0 {
branchKvs := make([]string, 0, len(m.branches))
for k, v := range m.branches {
branchKvs = append(branchKvs, fmt.Sprintf("%q: %s", k, v))
}
slices.Sort(branchKvs)
kvs = append(kvs, fmt.Sprintf("branches: {%s}", strings.Join(branchKvs, ", ")))
}
return fmt.Sprintf("{%s}", strings.Join(kvs, ", "))
}
func (m *muxTree) set(pattern string, fn response.HandlerFunc) {
log := m.log.With(
"method", "set",
"patter", pattern,
"fn", fn,
)
defer log.DebugCallf("called")()
method, segments, trailingSlash, endOfURLWildcard := getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(m.log, pattern)
m.setBySegments(
method,
segments,
trailingSlash,
endOfURLWildcard,
fn,
)
}
// TODO: handle /{$} properly
func (m *muxTree) setBySegments(method string, segments []string, trailingSlash, endOfURLWildcard bool, fn response.HandlerFunc) {
log := m.log.With(
"method", "setBySegments",
"arg.method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
"fn", fn,
)
defer log.DebugCallf("called")()
if len(segments) == 0 {
m.method = method
if !trailingSlash || endOfURLWildcard {
m.handler = fn
} else if trailingSlash {
m.subrouteHandler = fn
}
return
}
head := segments[0]
tail := segments[1:]
var sub *muxTree
if key, ok := getWildcardPathSegmentKey(head); ok {
m.wildcardKey = key
if sub = m.wildcardBranch; sub == nil {
sub = newMuxTree(m.log)
m.wildcardBranch = sub
}
} else {
if sub = m.branches[head]; sub == nil {
sub = newMuxTree(m.log)
m.branches[head] = sub
}
}
/*
if isWildcard, wildcardKey := isWildcardPathSegment(head); wildcardKey == "$" {
// TODO: test
// TODO: handle trailing end path matching
if len(tail) > 0 || trailingSlash {
panic("{$} wildcard applied outside of end of path")
}
m.method = method
m.handler = fn
return
} else if isWildcard {
m.wildcardKey = wildcardKey
if sub = m.wildcardBranch; sub == nil {
sub = newMuxTree(m.log)
m.wildcardBranch = sub
}
} else {
if sub = m.branches[head]; sub == nil {
sub = newMuxTree(m.log)
m.branches[head] = sub
}
}
*/
sub.setBySegments(method, tail, trailingSlash, endOfURLWildcard, fn)
}
func (m *muxTree) get(method, pattern string) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "get",
"arg.method", method,
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
segments, trailingSlash := getPathSegments(m.log, pattern)
return m.getByHTTPMethodAndSegments(method, segments, trailingSlash)
}
func (m *muxTree) getByHTTPMethodAndSegments(method string, segments []string, trailingSlash bool) (fn response.HandlerFunc, params map[string]string, found bool) {
log := m.log.With(
"method", "getByHTTPMethodAndSegments",
"arg.method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"muxTree", m,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"fn", fn,
"params", params,
"found", found,
}
})
if len(segments) == 0 {
log.Debugf("no segments")
if methodMatches := m.method == "" || m.method == method; methodMatches {
if fn = m.handler; fn == nil {
fn = m.subrouteHandler
}
return fn, map[string]string{}, fn != nil
}
return nil, map[string]string{}, false
}
head := segments[0]
tail := segments[1:]
if m.subrouteHandler != nil {
defer func() {
if !found && (m.method == "" || m.method == method) {
fn = m.subrouteHandler
found = true
}
}()
}
if sm, ok := m.branches[head]; ok {
log.Debugf("matching branch found")
//return sm.getByHTTPMethodAndSegments(method, tail, trailingSlash)
if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found {
return fn, params, found
}
log.Debugf("matching branch mismatched at subpath")
} else {
log.Debugf("no matching branch found")
}
if sm := m.wildcardBranch; sm != nil {
wlog := log.With(
"wildcardKey", m.wildcardKey,
"sub.muxTree", sm,
)
wlog.Debugf("wildcard branch found")
//fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash)
if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found {
params[m.wildcardKey] = head
return fn, params, found
}
wlog.Debugf("wildcard branch mismatched at subpath")
} else {
log.Debugf("no wildcard branch found")
}
// --- TODO: test ---
if m.subrouteHandler != nil && (m.method == method || m.method == "") {
log.Debugf("subroutes captured")
return m.subrouteHandler, nil, true
}
log.Debugf("subroutes not captured")
// ---
return nil, map[string]string{}, false
}
func getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash, endOfURLWildcard bool) {
log := logger.With(
"function", "getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard",
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"method", method,
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
}
})
if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 {
method = vs[0]
pattern = vs[1]
}
segments, trailingSlash, endOfURLWildcard = getPathSegmentsWithoutEndOfURLWildcard(logger, pattern)
return method, segments, trailingSlash, endOfURLWildcard
}
func getHTTPMethodAndPathSegments(logger *logging.Logger, pattern string) (method string, segments []string, trailingSlash bool) {
log := logger.With(
"function", "getHTTPMethodAndPathSegments",
"pattern", pattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"method", method,
"segments", segments,
"trailingSlash", trailingSlash,
}
})
if vs := strings.SplitN(pattern, " ", 2); len(vs) == 2 {
method = vs[0]
pattern = vs[1]
}
segments, trailingSlash = getPathSegments(logger, pattern)
return method, segments, trailingSlash
}
func getPathSegments(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash bool) {
log := logger.With(
"function", "getPathSegments",
"pathPattern", pathPattern,
)
defer log.DebugCallf("getPathSegments")()
trailingSlash = strings.HasSuffix(pathPattern, "/")
p := strings.Trim(path.Clean(pathPattern), "/")
if p == "" {
return nil, trailingSlash
}
return strings.Split(p, "/"), trailingSlash
}
// NOTE: if endOfURLWildcard, then trailingSlash
func getPathSegmentsWithoutEndOfURLWildcard(logger *logging.Logger, pathPattern string) (segments []string, trailingSlash, endOfURLWildcard bool) {
log := logger.With(
"function", "getPathSegmentsWithoutEndOfURLWildcard",
"pathPattern", pathPattern,
)
defer log.DebugDeferf("called")(func() (string, []any) {
return "returned", []any{
"segments", segments,
"trailingSlash", trailingSlash,
"endOfURLWildcard", endOfURLWildcard,
}
})
endOfURLWildcard = strings.HasSuffix(pathPattern, "/{$}")
if endOfURLWildcard {
pathPattern = pathPattern[:len(pathPattern)-3]
}
trailingSlash = strings.HasSuffix(pathPattern, "/")
p := strings.Trim(path.Clean(pathPattern), "/")
if p == "" {
return nil, trailingSlash, endOfURLWildcard
}
return strings.Split(p, "/"), trailingSlash, endOfURLWildcard
}
func getWildcardPathSegmentKey(s string) (string, bool) {
if isWildcard := len(s) > 2 && s[0] == '{' && s[len(s)-1] == '}'; isWildcard {
return s[1 : len(s)-1], true
}
return "", false
}
func applyMiddleware(fn response.HandlerFunc, ms ...response.Middleware) response.HandlerFunc {
for _, mw := range slices.Backward(ms) {
prev := fn
fn = mw(func(r *http.Request) (response.Response, error) {
return prev(r)
})
}
return fn
}
// TODO: remove the consts. trace level