mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
184 lines
5.6 KiB
Go
184 lines
5.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/container"
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
type RouterPathGroup struct {
|
|
r *Router
|
|
pathParam string
|
|
matchers []*routerPathMatcher
|
|
}
|
|
|
|
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
|
rc := GetRouteContext(req.Context())
|
|
path := rc.Param(g.pathParam)
|
|
for _, m := range g.matchers {
|
|
if m.matchPath(rc, path) {
|
|
rc.RoutePatterns = append(rc.RoutePatterns, m.pattern)
|
|
executeMiddlewaresHandler(resp, req, m.middlewares, m.handlerFunc)
|
|
return
|
|
}
|
|
}
|
|
g.r.mux.NotFoundHandler().ServeHTTP(resp, req)
|
|
}
|
|
|
|
type RouterPathGroupPattern struct {
|
|
pattern string
|
|
re *regexp.Regexp
|
|
params []routerPathParam
|
|
middlewares []any
|
|
}
|
|
|
|
// MatchPath matches the request method, and uses regexp to match the path.
|
|
// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router)
|
|
// It is only designed to resolve some special cases that chi router can't handle.
|
|
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
|
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
|
|
g.MatchPattern(methods, g.PatternRegexp(pattern), h...)
|
|
}
|
|
|
|
func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) {
|
|
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
|
}
|
|
|
|
type routerPathParam struct {
|
|
name string
|
|
pathSepEnd bool
|
|
captureGroup int
|
|
}
|
|
|
|
type routerPathMatcher struct {
|
|
methods container.Set[string]
|
|
pattern string
|
|
re *regexp.Regexp
|
|
params []routerPathParam
|
|
middlewares []middlewareProvider
|
|
handlerFunc http.HandlerFunc
|
|
}
|
|
|
|
func (p *routerPathMatcher) matchPath(rc *RouteContext, path string) bool {
|
|
if !p.methods.Contains(rc.RouteMethod) {
|
|
return false
|
|
}
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
|
if pathMatches == nil {
|
|
return false
|
|
}
|
|
var paramMatches [][]int
|
|
for i := 2; i < len(pathMatches); {
|
|
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
|
pmIdx := len(paramMatches) - 1
|
|
end := pathMatches[i+1]
|
|
i += 2
|
|
for ; i < len(pathMatches); i += 2 {
|
|
if pathMatches[i] >= end {
|
|
break
|
|
}
|
|
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
|
}
|
|
}
|
|
for i, pm := range paramMatches {
|
|
groupIdx := p.params[i].captureGroup * 2
|
|
if pm[groupIdx] == -1 || pm[groupIdx+1] == -1 {
|
|
rc.SetParam(p.params[i].name, "")
|
|
continue
|
|
}
|
|
val := path[pm[groupIdx]:pm[groupIdx+1]]
|
|
if p.params[i].pathSepEnd {
|
|
val = strings.TrimSuffix(val, "/")
|
|
}
|
|
rc.SetParam(p.params[i].name, val)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isValidMethod(name string) bool {
|
|
switch name {
|
|
case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions, http.MethodConnect, http.MethodTrace:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher {
|
|
middlewares, handlerFunc, hasPreMiddlewares := wrapMiddlewareAndHandler(nil, patternRegexp.middlewares, h)
|
|
if hasPreMiddlewares {
|
|
panic("pre-middlewares are not supported in router path matcher")
|
|
}
|
|
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
|
for method := range strings.SplitSeq(methods, ",") {
|
|
method = strings.TrimSpace(method)
|
|
if !isValidMethod(method) {
|
|
panic("invalid HTTP method: " + method)
|
|
}
|
|
p.methods.Add(method)
|
|
}
|
|
p.pattern, p.re, p.params = patternRegexp.pattern, patternRegexp.re, patternRegexp.params
|
|
return p
|
|
}
|
|
|
|
func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
|
|
p := &RouterPathGroupPattern{middlewares: slices.Clone(h)}
|
|
re := []byte{'^'}
|
|
lastEnd := 0
|
|
for lastEnd < len(pattern) {
|
|
start := strings.IndexByte(pattern[lastEnd:], '<')
|
|
if start == -1 {
|
|
re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...)
|
|
break
|
|
}
|
|
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
|
if end == -1 {
|
|
panic("invalid pattern: " + pattern)
|
|
}
|
|
re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...)
|
|
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
|
lastEnd += start + end + 1
|
|
|
|
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
|
// it is not used so no need to implement it now
|
|
param := routerPathParam{}
|
|
if partExp == "*" {
|
|
// "<part:*>" is a shorthand for optionally matching any string (but not greedy)
|
|
partExp = ".*?"
|
|
if lastEnd < len(pattern) && pattern[lastEnd] == '/' {
|
|
// if this param part ends with path separator "/", then consider it together: "(.*?/)"
|
|
partExp += "/"
|
|
param.pathSepEnd = true
|
|
lastEnd++
|
|
}
|
|
re = append(re, '(')
|
|
re = append(re, partExp...)
|
|
re = append(re, ')', '?') // the wildcard matching is optional
|
|
} else {
|
|
// the pattern is user-provided regexp, defaults to a path part (separated by "/")
|
|
partExp = util.IfZero(partExp, "[^/]+")
|
|
re = append(re, '(')
|
|
re = append(re, partExp...)
|
|
re = append(re, ')')
|
|
}
|
|
param.name = partName
|
|
p.params = append(p.params, param)
|
|
}
|
|
re = append(re, '$')
|
|
p.pattern, p.re = pattern, regexp.MustCompile(string(re))
|
|
return p
|
|
}
|
|
|
|
func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern {
|
|
return patternRegexp(pattern, h...)
|
|
}
|