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