package router import ( "fmt" "log/slog" "net/http" "path" "slices" "strings" "ruben/inventory2/internal/consts" "ruben/inventory2/internal/server/response" ) type ( Mux struct { Mux *http.ServeMux middleware []response.Middleware log *slog.Logger } ) var ( ErrHandlerNotFound = fmt.Errorf("%w: handler not found", consts.ErrNotFound) ) func NewMux(log *slog.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) traced(method, msg string, args ...any) func(func() (finalArgs []any)) { return logTraceAndDefer(m.log, "Mux", method, msg, args...) } func (m *Mux) Handle(pattern string, fn response.HandlerFunc) { defer m.traced("Handle", "", "pattern", pattern)(nil) 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) { defer m.traced("Route", "pattern", pattern)(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 } logTrace(m.log, "mux", "Route", "cleanPattern: "+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 *slog.Logger } ) func NewSubMux(log *slog.Logger, ms ...response.Middleware) *SubMux { return &SubMux{ tree: newMuxTree(log.WithGroup("muxTree")), middleware: ms, log: log, } } func (m *SubMux) traced(method, msg string, args ...any) func(func() (finalArgs []any)) { return logTraceAndDefer(m.log, "SubMux", method, msg, args...) } // TODO: this is capturing all subroutes! func (m *SubMux) Handle(pattern string, fn response.HandlerFunc) { defer m.traced("Handle", "", "pattern", pattern)(nil) 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) { //defer m.traced("Route", "", "basePathPattern", basePathPattern)(nil) defer m.traced("Route", "", "pattern", pattern)(nil) 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 } logTrace(m.log, "SubMux", "Route", "Handle about to be called", "cleanPattern", 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 *slog.Logger, numPatternSegments int, sr Subrouter) response.HandlerFunc { defer logTraceAndDefer(logger, "", "buildSubrouterHandlerFunc", "", "numPatternSegments", numPatternSegments)(nil) return func(r *http.Request) (res response.Response, err error) { defer logTraceAndDefer(logger, "", "buildSubrouterHandlerFunc", "", "numPatternSegments", numPatternSegments, "r.URL", r.URL)(func() []any { return []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 logTrace(logger, "", "buildSubrouterHandlerFunc", "handler called", "subpath", r.URL.Path) fn, params, ok := sr.Handler(r) logTrace(logger, "", "buildSubrouterHandlerFunc", "handler returned", "subpath", r.URL.Path, "fn", fn, "params", params, "ok", 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) { defer m.traced("Handler", "", "r.URL", r.URL)(func() []any { return []any{ "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 *slog.Logger branches map[string]*muxTree wildcardKey string wildcardBranch *muxTree method string handler response.HandlerFunc subrouteHandler response.HandlerFunc } ) func newMuxTree(log *slog.Logger) *muxTree { return &muxTree{ log: log, branches: make(map[string]*muxTree), } } func (m *muxTree) traced(method, msg string, args ...any) func(func() (finalArgs []any)) { return logTraceAndDefer(m.log, "muxTree", method, msg, args...) } func (m *muxTree) String() string { if m == nil { return "" } 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) { defer m.traced("set", "", "pattern", pattern)(func() []any { return []any{ "muxTree", m, } }) 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) { defer m.traced( "setBySegments", "", "method", method, "segments", segments, "trailingSlash", trailingSlash, "endOfURLWildcard", endOfURLWildcard, )(func() []any { return []any{ "muxTree", m, } }) 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) { defer m.traced("get", "", "method", method, "pattern", pattern)(func() []any { return []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) { defer m.traced("getByHTTPMethodAndSegments", "", "method", method, "segments", segments, "trailingSlash", trailingSlash)(func() []any { return []any{ "fn", fn, "params", params, "found", found, } }) if len(segments) == 0 { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no segments", "tree", m) 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 { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "matching branch found", "head", head, "sm", sm) //return sm.getByHTTPMethodAndSegments(method, tail, trailingSlash) if fn, params, found = sm.getByHTTPMethodAndSegments(method, tail, trailingSlash); found { return fn, params, found } logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "matching branch mismatched at subpath") } else { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no matching branch found") } if sm := m.wildcardBranch; sm != nil { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "wildcard branch found", "m.wildcardKey", m.wildcardKey, "sm", sm) //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 } logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "wildcard branch mismatched at subpath") } else { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "no wildcard branch found") } // --- TODO: test --- if m.subrouteHandler != nil && (m.method == method || m.method == "") { logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "subroutes captured", "m", m) return m.subrouteHandler, nil, true } logTrace(m.log, "muxTree", "getByHTTPMethodAndSegments", "subroutes not captured", "m", m) // --- return nil, map[string]string{}, false } func getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard(logger *slog.Logger, pattern string) (method string, segments []string, trailingSlash, endOfURLWildcard bool) { defer logTraceAndDefer(logger, "", "getHTTPMethodAndPathSegmentsDroppingEndOfURLWildcard", "", "pattern", pattern)(func() []any { return []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 *slog.Logger, pattern string) (method string, segments []string, trailingSlash bool) { defer logTraceAndDefer(logger, "", "getHTTPMethodAndPathSegments", "", "pattern", pattern)(func() []any { return []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 *slog.Logger, pathPattern string) (segments []string, trailingSlash bool) { defer logTraceAndDefer(logger, "", "getPathSegments", "", "pathPattern", pathPattern)(func() []any { return []any{ "segments", segments, "trailingSlash", trailingSlash, } }) 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 *slog.Logger, pathPattern string) (segments []string, trailingSlash, endOfURLWildcard bool) { defer logTraceAndDefer(logger, "", "getPathSegmentsWithoutEndOfURLWildcard", "", "pathPattern", pathPattern)(func() []any { return []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 } // logging utilities func logTraceAndDefer(logger *slog.Logger, typeName, method, msg string, args ...any) func(func() (finalArgs []any)) { fmsg := fmt.Sprintf("%s.%s called", typeName, method) if msg != "" { fmsg = fmt.Sprintf("%s: %s", fmsg, msg) } logger.Log(nil, consts.LevelTrace, fmsg, args...) return func(cb func() (finalArgs []any)) { if cb != nil { args = append(args, cb()...) } fmsg := fmt.Sprintf("%s.%s returned", typeName, method) if msg != "" { fmsg = fmt.Sprintf("%s: %s", fmsg, msg) } logger.Log(nil, consts.LevelTrace, fmsg, args...) } } func logTrace(logger *slog.Logger, typeName, method, msg string, args ...any) { fmsg := fmt.Sprintf("%s.%s", typeName, method) if msg != "" { fmsg = fmt.Sprintf("%s: %s", fmsg, msg) } logger.Log(nil, consts.LevelTrace, fmsg, args...) }