Skip to content

Commit e435b19

Browse files
authored
Refactor arch route handlers (#32993)
1 parent 254314b commit e435b19

File tree

5 files changed

+260
-190
lines changed

5 files changed

+260
-190
lines changed

modules/web/route.go renamed to modules/web/router.go

Lines changed: 19 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@
44
package web
55

66
import (
7-
"fmt"
87
"net/http"
98
"net/url"
109
"reflect"
11-
"regexp"
1210
"strings"
1311

14-
"code.gitea.io/gitea/modules/container"
1512
"code.gitea.io/gitea/modules/htmlutil"
1613
"code.gitea.io/gitea/modules/reqctx"
1714
"code.gitea.io/gitea/modules/setting"
18-
"code.gitea.io/gitea/modules/util"
1915
"code.gitea.io/gitea/modules/web/middleware"
2016

2117
"gitea.com/go-chi/binding"
@@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any {
4541

4642
// Router defines a route based on chi's router
4743
type Router struct {
48-
chiRouter chi.Router
44+
chiRouter *chi.Mux
4945
curGroupPrefix string
5046
curMiddlewares []any
5147
}
@@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool {
9793
return r.Kind() == reflect.Func && r.IsNil()
9894
}
9995

100-
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
101-
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
102-
for _, m := range r.curMiddlewares {
96+
func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
97+
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1)
98+
for _, m := range curMiddlewares {
10399
if !isNilOrFuncNil(m) {
104100
handlerProviders = append(handlerProviders, toHandlerProvider(m))
105101
}
106102
}
107-
for _, m := range h {
103+
if len(h) == 0 {
104+
panic("no endpoint handler provided")
105+
}
106+
for i, m := range h {
108107
if !isNilOrFuncNil(m) {
109108
handlerProviders = append(handlerProviders, toHandlerProvider(m))
109+
} else if i == len(h)-1 {
110+
panic("endpoint handler can't be nil")
110111
}
111112
}
112113
middlewares := handlerProviders[:len(handlerProviders)-1]
@@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha
121122
// Methods adds the same handlers for multiple http "methods" (separated by ",").
122123
// If any method is invalid, the lower level router will panic.
123124
func (r *Router) Methods(methods, pattern string, h ...any) {
124-
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
125+
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
125126
fullPattern := r.getPattern(pattern)
126127
if strings.Contains(methods, ",") {
127128
methods := strings.Split(methods, ",")
@@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) {
141142

142143
// Any delegate requests for all methods
143144
func (r *Router) Any(pattern string, h ...any) {
144-
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
145+
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
145146
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
146147
}
147148

@@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) {
185186
r.chiRouter.NotFound(h)
186187
}
187188

188-
type pathProcessorParam struct {
189-
name string
190-
captureGroup int
191-
}
192-
193-
type PathProcessor struct {
194-
methods container.Set[string]
195-
re *regexp.Regexp
196-
params []pathProcessorParam
197-
}
198-
199189
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
200190
normalized := false
201191
normalizedPath := req.URL.EscapedPath()
@@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
253243
next.ServeHTTP(resp, req)
254244
}
255245

256-
func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool {
257-
if !p.methods.Contains(chiCtx.RouteMethod) {
258-
return false
259-
}
260-
if !strings.HasPrefix(path, "/") {
261-
path = "/" + path
262-
}
263-
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
264-
if pathMatches == nil {
265-
return false
266-
}
267-
var paramMatches [][]int
268-
for i := 2; i < len(pathMatches); {
269-
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
270-
pmIdx := len(paramMatches) - 1
271-
end := pathMatches[i+1]
272-
i += 2
273-
for ; i < len(pathMatches); i += 2 {
274-
if pathMatches[i] >= end {
275-
break
276-
}
277-
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
278-
}
279-
}
280-
for i, pm := range paramMatches {
281-
groupIdx := p.params[i].captureGroup * 2
282-
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
283-
}
284-
return true
285-
}
286-
287-
func NewPathProcessor(methods, pattern string) *PathProcessor {
288-
p := &PathProcessor{methods: make(container.Set[string])}
289-
for _, method := range strings.Split(methods, ",") {
290-
p.methods.Add(strings.TrimSpace(method))
291-
}
292-
re := []byte{'^'}
293-
lastEnd := 0
294-
for lastEnd < len(pattern) {
295-
start := strings.IndexByte(pattern[lastEnd:], '<')
296-
if start == -1 {
297-
re = append(re, pattern[lastEnd:]...)
298-
break
299-
}
300-
end := strings.IndexByte(pattern[lastEnd+start:], '>')
301-
if end == -1 {
302-
panic(fmt.Sprintf("invalid pattern: %s", pattern))
303-
}
304-
re = append(re, pattern[lastEnd:lastEnd+start]...)
305-
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
306-
lastEnd += start + end + 1
307-
308-
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
309-
// it is not used so no need to implement it now
310-
param := pathProcessorParam{}
311-
if partExp == "*" {
312-
re = append(re, "(.*?)/?"...)
313-
if lastEnd < len(pattern) {
314-
if pattern[lastEnd] == '/' {
315-
lastEnd++
316-
}
317-
}
318-
} else {
319-
partExp = util.IfZero(partExp, "[^/]+")
320-
re = append(re, '(')
321-
re = append(re, partExp...)
322-
re = append(re, ')')
323-
}
324-
param.name = partName
325-
p.params = append(p.params, param)
326-
}
327-
re = append(re, '$')
328-
reStr := string(re)
329-
p.re = regexp.MustCompile(reStr)
330-
return p
331-
}
332-
333246
// Combo delegates requests to Combo
334247
func (r *Router) Combo(pattern string, h ...any) *Combo {
335248
return &Combo{r, pattern, h}
336249
}
337250

338-
// Combo represents a tiny group routes with same pattern
339-
type Combo struct {
340-
r *Router
341-
pattern string
342-
h []any
343-
}
344-
345-
// Get delegates Get method
346-
func (c *Combo) Get(h ...any) *Combo {
347-
c.r.Get(c.pattern, append(c.h, h...)...)
348-
return c
349-
}
350-
351-
// Post delegates Post method
352-
func (c *Combo) Post(h ...any) *Combo {
353-
c.r.Post(c.pattern, append(c.h, h...)...)
354-
return c
355-
}
356-
357-
// Delete delegates Delete method
358-
func (c *Combo) Delete(h ...any) *Combo {
359-
c.r.Delete(c.pattern, append(c.h, h...)...)
360-
return c
361-
}
362-
363-
// Put delegates Put method
364-
func (c *Combo) Put(h ...any) *Combo {
365-
c.r.Put(c.pattern, append(c.h, h...)...)
366-
return c
367-
}
368-
369-
// Patch delegates Patch method
370-
func (c *Combo) Patch(h ...any) *Combo {
371-
c.r.Patch(c.pattern, append(c.h, h...)...)
372-
return c
251+
// PathGroup creates a group of paths which could be matched by regexp.
252+
// It is only designed to resolve some special cases which chi router can't handle.
253+
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
254+
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
255+
g := &RouterPathGroup{r: r, pathParam: "*"}
256+
fn(g)
257+
r.Any(pattern, append(h, g.ServeHTTP)...)
373258
}

modules/web/router_combo.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package web
5+
6+
// Combo represents a tiny group routes with same pattern
7+
type Combo struct {
8+
r *Router
9+
pattern string
10+
h []any
11+
}
12+
13+
// Get delegates Get method
14+
func (c *Combo) Get(h ...any) *Combo {
15+
c.r.Get(c.pattern, append(c.h, h...)...)
16+
return c
17+
}
18+
19+
// Post delegates Post method
20+
func (c *Combo) Post(h ...any) *Combo {
21+
c.r.Post(c.pattern, append(c.h, h...)...)
22+
return c
23+
}
24+
25+
// Delete delegates Delete method
26+
func (c *Combo) Delete(h ...any) *Combo {
27+
c.r.Delete(c.pattern, append(c.h, h...)...)
28+
return c
29+
}
30+
31+
// Put delegates Put method
32+
func (c *Combo) Put(h ...any) *Combo {
33+
c.r.Put(c.pattern, append(c.h, h...)...)
34+
return c
35+
}
36+
37+
// Patch delegates Patch method
38+
func (c *Combo) Patch(h ...any) *Combo {
39+
c.r.Patch(c.pattern, append(c.h, h...)...)
40+
return c
41+
}

modules/web/router_path.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package web
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"regexp"
10+
"strings"
11+
12+
"code.gitea.io/gitea/modules/container"
13+
"code.gitea.io/gitea/modules/util"
14+
15+
"github.com/go-chi/chi/v5"
16+
)
17+
18+
type RouterPathGroup struct {
19+
r *Router
20+
pathParam string
21+
matchers []*routerPathMatcher
22+
}
23+
24+
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
25+
chiCtx := chi.RouteContext(req.Context())
26+
path := chiCtx.URLParam(g.pathParam)
27+
for _, m := range g.matchers {
28+
if m.matchPath(chiCtx, path) {
29+
handler := m.handlerFunc
30+
for i := len(m.middlewares) - 1; i >= 0; i-- {
31+
handler = m.middlewares[i](handler).ServeHTTP
32+
}
33+
handler(resp, req)
34+
return
35+
}
36+
}
37+
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
38+
}
39+
40+
// MatchPath matches the request method, and uses regexp to match the path.
41+
// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router)
42+
// It is only designed to resolve some special cases which chi router can't handle.
43+
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
44+
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
45+
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
46+
}
47+
48+
type routerPathParam struct {
49+
name string
50+
captureGroup int
51+
}
52+
53+
type routerPathMatcher struct {
54+
methods container.Set[string]
55+
re *regexp.Regexp
56+
params []routerPathParam
57+
middlewares []func(http.Handler) http.Handler
58+
handlerFunc http.HandlerFunc
59+
}
60+
61+
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
62+
if !p.methods.Contains(chiCtx.RouteMethod) {
63+
return false
64+
}
65+
if !strings.HasPrefix(path, "/") {
66+
path = "/" + path
67+
}
68+
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
69+
if pathMatches == nil {
70+
return false
71+
}
72+
var paramMatches [][]int
73+
for i := 2; i < len(pathMatches); {
74+
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
75+
pmIdx := len(paramMatches) - 1
76+
end := pathMatches[i+1]
77+
i += 2
78+
for ; i < len(pathMatches); i += 2 {
79+
if pathMatches[i] >= end {
80+
break
81+
}
82+
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
83+
}
84+
}
85+
for i, pm := range paramMatches {
86+
groupIdx := p.params[i].captureGroup * 2
87+
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
88+
}
89+
return true
90+
}
91+
92+
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
93+
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
94+
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
95+
for _, method := range strings.Split(methods, ",") {
96+
p.methods.Add(strings.TrimSpace(method))
97+
}
98+
re := []byte{'^'}
99+
lastEnd := 0
100+
for lastEnd < len(pattern) {
101+
start := strings.IndexByte(pattern[lastEnd:], '<')
102+
if start == -1 {
103+
re = append(re, pattern[lastEnd:]...)
104+
break
105+
}
106+
end := strings.IndexByte(pattern[lastEnd+start:], '>')
107+
if end == -1 {
108+
panic(fmt.Sprintf("invalid pattern: %s", pattern))
109+
}
110+
re = append(re, pattern[lastEnd:lastEnd+start]...)
111+
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
112+
lastEnd += start + end + 1
113+
114+
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
115+
// it is not used so no need to implement it now
116+
param := routerPathParam{}
117+
if partExp == "*" {
118+
re = append(re, "(.*?)/?"...)
119+
if lastEnd < len(pattern) && pattern[lastEnd] == '/' {
120+
lastEnd++ // the "*" pattern is able to handle the last slash, so skip it
121+
}
122+
} else {
123+
partExp = util.IfZero(partExp, "[^/]+")
124+
re = append(re, '(')
125+
re = append(re, partExp...)
126+
re = append(re, ')')
127+
}
128+
param.name = partName
129+
p.params = append(p.params, param)
130+
}
131+
re = append(re, '$')
132+
reStr := string(re)
133+
p.re = regexp.MustCompile(reStr)
134+
return p
135+
}

0 commit comments

Comments
 (0)