mirror of
https://github.com/go-gitea/gitea
synced 2025-07-03 09:07:19 +00:00
Move macaron to chi (#14293)
Use [chi](https://github.com/go-chi/chi) instead of the forked [macaron](https://gitea.com/macaron/macaron). Since macaron and chi have conflicts with session share, this big PR becomes a have-to thing. According my previous idea, we can replace macaron step by step but I'm wrong. :( Below is a list of big changes on this PR. - [x] Define `context.ResponseWriter` interface with an implementation `context.Response`. - [x] Use chi instead of macaron, and also a customize `Route` to wrap chi so that the router usage is similar as before. - [x] Create different routers for `web`, `api`, `internal` and `install` so that the codes will be more clear and no magic . - [x] Use https://github.com/unrolled/render instead of macaron's internal render - [x] Use https://github.com/NYTimes/gziphandler instead of https://gitea.com/macaron/gzip - [x] Use https://gitea.com/go-chi/session which is a modified version of https://gitea.com/macaron/session and removed `nodb` support since it will not be maintained. **BREAK** - [x] Use https://gitea.com/go-chi/captcha which is a modified version of https://gitea.com/macaron/captcha - [x] Use https://gitea.com/go-chi/cache which is a modified version of https://gitea.com/macaron/cache - [x] Use https://gitea.com/go-chi/binding which is a modified version of https://gitea.com/macaron/binding - [x] Use https://github.com/go-chi/cors instead of https://gitea.com/macaron/cors - [x] Dropped https://gitea.com/macaron/i18n and make a new one in `code.gitea.io/gitea/modules/translation` - [x] Move validation form structs from `code.gitea.io/gitea/modules/auth` to `code.gitea.io/gitea/modules/forms` to avoid dependency cycle. - [x] Removed macaron log service because it's not need any more. **BREAK** - [x] All form structs have to be get by `web.GetForm(ctx)` in the route function but not as a function parameter on routes definition. - [x] Move Git HTTP protocol implementation to use routers directly. - [x] Fix the problem that chi routes don't support trailing slash but macaron did. - [x] `/api/v1/swagger` now will be redirect to `/api/swagger` but not render directly so that `APIContext` will not create a html render. Notices: - Chi router don't support request with trailing slash - Integration test `TestUserHeatmap` maybe mysql version related. It's failed on my macOS(mysql 5.7.29 installed via brew) but succeed on CI. Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
322
modules/web/route.go
Normal file
322
modules/web/route.go
Normal file
@ -0,0 +1,322 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/middlewares"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
// Wrap converts all kinds of routes to standard library one
|
||||
func Wrap(handlers ...interface{}) http.HandlerFunc {
|
||||
if len(handlers) == 0 {
|
||||
panic("No handlers found")
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
switch t := handler.(type) {
|
||||
case http.HandlerFunc, func(http.ResponseWriter, *http.Request),
|
||||
func(ctx *context.Context),
|
||||
func(*context.APIContext),
|
||||
func(*context.PrivateContext),
|
||||
func(http.Handler) http.Handler:
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported handler type: %#v", t))
|
||||
}
|
||||
}
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
for i := 0; i < len(handlers); i++ {
|
||||
handler := handlers[i]
|
||||
switch t := handler.(type) {
|
||||
case http.HandlerFunc:
|
||||
t(resp, req)
|
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 {
|
||||
return
|
||||
}
|
||||
case func(http.ResponseWriter, *http.Request):
|
||||
t(resp, req)
|
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 {
|
||||
return
|
||||
}
|
||||
case func(ctx *context.Context):
|
||||
ctx := context.GetContext(req)
|
||||
t(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
case func(*context.APIContext):
|
||||
ctx := context.GetAPIContext(req)
|
||||
t(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
case func(*context.PrivateContext):
|
||||
ctx := context.GetPrivateContext(req)
|
||||
t(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
case func(http.Handler) http.Handler:
|
||||
var next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
t(next).ServeHTTP(resp, req)
|
||||
if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 {
|
||||
return
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported handler type: %#v", t))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Middle wrap a context function as a chi middleware
|
||||
func Middle(f func(ctx *context.Context)) func(netx http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.GetContext(req)
|
||||
f(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MiddleAPI wrap a context function as a chi middleware
|
||||
func MiddleAPI(f func(ctx *context.APIContext)) func(netx http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
ctx := context.GetAPIContext(req)
|
||||
f(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Bind binding an obj to a handler
|
||||
func Bind(obj interface{}) http.HandlerFunc {
|
||||
var tp = reflect.TypeOf(obj)
|
||||
if tp.Kind() == reflect.Ptr {
|
||||
tp = tp.Elem()
|
||||
}
|
||||
if tp.Kind() != reflect.Struct {
|
||||
panic("Only structs are allowed to bind")
|
||||
}
|
||||
return Wrap(func(ctx *context.Context) {
|
||||
var theObj = reflect.New(tp).Interface() // create a new form obj for every request but not use obj directly
|
||||
binding.Bind(ctx.Req, theObj)
|
||||
SetForm(ctx, theObj)
|
||||
middlewares.AssignForm(theObj, ctx.Data)
|
||||
})
|
||||
}
|
||||
|
||||
// SetForm set the form object
|
||||
func SetForm(data middlewares.DataStore, obj interface{}) {
|
||||
data.GetData()["__form"] = obj
|
||||
}
|
||||
|
||||
// GetForm returns the validate form information
|
||||
func GetForm(data middlewares.DataStore) interface{} {
|
||||
return data.GetData()["__form"]
|
||||
}
|
||||
|
||||
// Route defines a route based on chi's router
|
||||
type Route struct {
|
||||
R chi.Router
|
||||
curGroupPrefix string
|
||||
curMiddlewares []interface{}
|
||||
}
|
||||
|
||||
// NewRoute creates a new route
|
||||
func NewRoute() *Route {
|
||||
r := chi.NewRouter()
|
||||
return &Route{
|
||||
R: r,
|
||||
curGroupPrefix: "",
|
||||
curMiddlewares: []interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// Use supports two middlewares
|
||||
func (r *Route) Use(middlewares ...interface{}) {
|
||||
if r.curGroupPrefix != "" {
|
||||
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
|
||||
} else {
|
||||
for _, middle := range middlewares {
|
||||
switch t := middle.(type) {
|
||||
case func(http.Handler) http.Handler:
|
||||
r.R.Use(t)
|
||||
case func(*context.Context):
|
||||
r.R.Use(Middle(t))
|
||||
case func(*context.APIContext):
|
||||
r.R.Use(MiddleAPI(t))
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported middleware type: %#v", t))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Group mounts a sub-Router along a `pattern` string.
|
||||
func (r *Route) Group(pattern string, fn func(), middlewares ...interface{}) {
|
||||
var previousGroupPrefix = r.curGroupPrefix
|
||||
var previousMiddlewares = r.curMiddlewares
|
||||
r.curGroupPrefix += pattern
|
||||
r.curMiddlewares = append(r.curMiddlewares, middlewares...)
|
||||
|
||||
fn()
|
||||
|
||||
r.curGroupPrefix = previousGroupPrefix
|
||||
r.curMiddlewares = previousMiddlewares
|
||||
}
|
||||
|
||||
func (r *Route) getPattern(pattern string) string {
|
||||
newPattern := r.curGroupPrefix + pattern
|
||||
if !strings.HasPrefix(newPattern, "/") {
|
||||
newPattern = "/" + newPattern
|
||||
}
|
||||
if newPattern == "/" {
|
||||
return newPattern
|
||||
}
|
||||
return strings.TrimSuffix(newPattern, "/")
|
||||
}
|
||||
|
||||
// Mount attaches another Route along ./pattern/*
|
||||
func (r *Route) Mount(pattern string, subR *Route) {
|
||||
var middlewares = make([]interface{}, len(r.curMiddlewares))
|
||||
copy(middlewares, r.curMiddlewares)
|
||||
subR.Use(middlewares...)
|
||||
r.R.Mount(r.getPattern(pattern), subR.R)
|
||||
}
|
||||
|
||||
// Any delegate requests for all methods
|
||||
func (r *Route) Any(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.HandleFunc(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// Route delegate special methods
|
||||
func (r *Route) Route(pattern string, methods string, h ...interface{}) {
|
||||
p := r.getPattern(pattern)
|
||||
ms := strings.Split(methods, ",")
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
for _, method := range ms {
|
||||
r.R.MethodFunc(strings.TrimSpace(method), p, Wrap(middlewares...))
|
||||
}
|
||||
}
|
||||
|
||||
// Delete delegate delete method
|
||||
func (r *Route) Delete(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Delete(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
func (r *Route) getMiddlewares(h []interface{}) []interface{} {
|
||||
var middlewares = make([]interface{}, len(r.curMiddlewares), len(r.curMiddlewares)+len(h))
|
||||
copy(middlewares, r.curMiddlewares)
|
||||
middlewares = append(middlewares, h...)
|
||||
return middlewares
|
||||
}
|
||||
|
||||
// Get delegate get method
|
||||
func (r *Route) Get(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Get(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// Head delegate head method
|
||||
func (r *Route) Head(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Head(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// Post delegate post method
|
||||
func (r *Route) Post(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Post(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// Put delegate put method
|
||||
func (r *Route) Put(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Put(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// Patch delegate patch method
|
||||
func (r *Route) Patch(pattern string, h ...interface{}) {
|
||||
var middlewares = r.getMiddlewares(h)
|
||||
r.R.Patch(r.getPattern(pattern), Wrap(middlewares...))
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler
|
||||
func (r *Route) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
r.R.ServeHTTP(w, req)
|
||||
}
|
||||
|
||||
// NotFound defines a handler to respond whenever a route could
|
||||
// not be found.
|
||||
func (r *Route) NotFound(h http.HandlerFunc) {
|
||||
r.R.NotFound(h)
|
||||
}
|
||||
|
||||
// MethodNotAllowed defines a handler to respond whenever a method is
|
||||
// not allowed.
|
||||
func (r *Route) MethodNotAllowed(h http.HandlerFunc) {
|
||||
r.R.MethodNotAllowed(h)
|
||||
}
|
||||
|
||||
// Combo deletegate requests to Combo
|
||||
func (r *Route) Combo(pattern string, h ...interface{}) *Combo {
|
||||
return &Combo{r, pattern, h}
|
||||
}
|
||||
|
||||
// Combo represents a tiny group routes with same pattern
|
||||
type Combo struct {
|
||||
r *Route
|
||||
pattern string
|
||||
h []interface{}
|
||||
}
|
||||
|
||||
// Get deletegate Get method
|
||||
func (c *Combo) Get(h ...interface{}) *Combo {
|
||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Post deletegate Post method
|
||||
func (c *Combo) Post(h ...interface{}) *Combo {
|
||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Delete deletegate Delete method
|
||||
func (c *Combo) Delete(h ...interface{}) *Combo {
|
||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Put deletegate Put method
|
||||
func (c *Combo) Put(h ...interface{}) *Combo {
|
||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Patch deletegate Patch method
|
||||
func (c *Combo) Patch(h ...interface{}) *Combo {
|
||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
169
modules/web/route_test.go
Normal file
169
modules/web/route_test.go
Normal file
@ -0,0 +1,169 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRoute1(t *testing.T) {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
recorder.Body = buff
|
||||
|
||||
r := NewRoute()
|
||||
r.Get("/{username}/{reponame}/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) {
|
||||
username := chi.URLParam(req, "username")
|
||||
assert.EqualValues(t, "gitea", username)
|
||||
reponame := chi.URLParam(req, "reponame")
|
||||
assert.EqualValues(t, "gitea", reponame)
|
||||
tp := chi.URLParam(req, "type")
|
||||
assert.EqualValues(t, "issues", tp)
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
func TestRoute2(t *testing.T) {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
recorder.Body = buff
|
||||
|
||||
var route int
|
||||
|
||||
r := NewRoute()
|
||||
r.Group("/{username}/{reponame}", func() {
|
||||
r.Group("", func() {
|
||||
r.Get("/{type:issues|pulls}", func(resp http.ResponseWriter, req *http.Request) {
|
||||
username := chi.URLParam(req, "username")
|
||||
assert.EqualValues(t, "gitea", username)
|
||||
reponame := chi.URLParam(req, "reponame")
|
||||
assert.EqualValues(t, "gitea", reponame)
|
||||
tp := chi.URLParam(req, "type")
|
||||
assert.EqualValues(t, "issues", tp)
|
||||
route = 0
|
||||
})
|
||||
|
||||
r.Get("/{type:issues|pulls}/{index}", func(resp http.ResponseWriter, req *http.Request) {
|
||||
username := chi.URLParam(req, "username")
|
||||
assert.EqualValues(t, "gitea", username)
|
||||
reponame := chi.URLParam(req, "reponame")
|
||||
assert.EqualValues(t, "gitea", reponame)
|
||||
tp := chi.URLParam(req, "type")
|
||||
assert.EqualValues(t, "issues", tp)
|
||||
index := chi.URLParam(req, "index")
|
||||
assert.EqualValues(t, "1", index)
|
||||
route = 1
|
||||
})
|
||||
}, func(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.WriteHeader(200)
|
||||
})
|
||||
|
||||
r.Group("/issues/{index}", func() {
|
||||
r.Get("/view", func(resp http.ResponseWriter, req *http.Request) {
|
||||
username := chi.URLParam(req, "username")
|
||||
assert.EqualValues(t, "gitea", username)
|
||||
reponame := chi.URLParam(req, "reponame")
|
||||
assert.EqualValues(t, "gitea", reponame)
|
||||
index := chi.URLParam(req, "index")
|
||||
assert.EqualValues(t, "1", index)
|
||||
route = 2
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 0, route)
|
||||
|
||||
req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 1, route)
|
||||
|
||||
req, err = http.NewRequest("GET", "http://localhost:8000/gitea/gitea/issues/1/view", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 2, route)
|
||||
}
|
||||
|
||||
func TestRoute3(t *testing.T) {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
recorder.Body = buff
|
||||
|
||||
var route int
|
||||
|
||||
m := NewRoute()
|
||||
r := NewRoute()
|
||||
r.Mount("/api/v1", m)
|
||||
|
||||
m.Group("/repos", func() {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Group("/branch_protections", func() {
|
||||
m.Get("", func(resp http.ResponseWriter, req *http.Request) {
|
||||
route = 0
|
||||
})
|
||||
m.Post("", func(resp http.ResponseWriter, req *http.Request) {
|
||||
route = 1
|
||||
})
|
||||
m.Group("/{name}", func() {
|
||||
m.Get("", func(resp http.ResponseWriter, req *http.Request) {
|
||||
route = 2
|
||||
})
|
||||
m.Patch("", func(resp http.ResponseWriter, req *http.Request) {
|
||||
route = 3
|
||||
})
|
||||
m.Delete("", func(resp http.ResponseWriter, req *http.Request) {
|
||||
route = 4
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 0, route)
|
||||
|
||||
req, err = http.NewRequest("POST", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code, http.StatusOK)
|
||||
assert.EqualValues(t, 1, route)
|
||||
|
||||
req, err = http.NewRequest("GET", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 2, route)
|
||||
|
||||
req, err = http.NewRequest("PATCH", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 3, route)
|
||||
|
||||
req, err = http.NewRequest("DELETE", "http://localhost:8000/api/v1/repos/gitea/gitea/branch_protections/master", nil)
|
||||
assert.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.EqualValues(t, http.StatusOK, recorder.Code)
|
||||
assert.EqualValues(t, 4, route)
|
||||
}
|
Reference in New Issue
Block a user