mirror of https://github.com/gin-gonic/gin.git
Merge branch 'master' into master
This commit is contained in:
commit
e00b80a591
|
@ -3,8 +3,6 @@ language: go
|
|||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- go: 1.8.x
|
||||
- go: 1.9.x
|
||||
- go: 1.10.x
|
||||
- go: 1.11.x
|
||||
env: GO111MODULE=on
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
### Gin 1.4.0
|
||||
|
||||
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
|
||||
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
||||
- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
|
||||
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
|
||||
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
|
||||
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)
|
||||
|
|
103
README.md
103
README.md
|
@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||
- [Only Bind Query String](#only-bind-query-string)
|
||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||
- [Bind Uri](#bind-uri)
|
||||
- [Bind Header](#bind-header)
|
||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
|
||||
|
@ -69,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||
|
||||
To install Gin package, you need to install Go and set your Go workspace first.
|
||||
|
||||
1. The first need [Go](https://golang.org/) installed (**version 1.8+ is required**), then you can use the below Go command to install Gin.
|
||||
1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin.
|
||||
|
||||
```sh
|
||||
$ go get -u github.com/gin-gonic/gin
|
||||
|
@ -252,6 +253,11 @@ func main() {
|
|||
c.String(http.StatusOK, message)
|
||||
})
|
||||
|
||||
// For each matched request Context will hold the route definition
|
||||
router.POST("/user/:name/*action", func(c *gin.Context) {
|
||||
c.FullPath() == "/user/:name/*action" // true
|
||||
})
|
||||
|
||||
router.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
@ -750,7 +756,7 @@ func bookableDate(
|
|||
) bool {
|
||||
if date, ok := field.Interface().(time.Time); ok {
|
||||
today := time.Now()
|
||||
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
||||
if today.After(date) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -843,6 +849,8 @@ type Person struct {
|
|||
Name string `form:"name"`
|
||||
Address string `form:"address"`
|
||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -860,6 +868,8 @@ func startPage(c *gin.Context) {
|
|||
log.Println(person.Name)
|
||||
log.Println(person.Address)
|
||||
log.Println(person.Birthday)
|
||||
log.Println(person.CreateTime)
|
||||
log.Println(person.UnixTime)
|
||||
}
|
||||
|
||||
c.String(200, "Success")
|
||||
|
@ -868,7 +878,7 @@ func startPage(c *gin.Context) {
|
|||
|
||||
Test it with:
|
||||
```sh
|
||||
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
|
||||
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
||||
```
|
||||
|
||||
### Bind Uri
|
||||
|
@ -905,6 +915,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
|
|||
$ curl -v localhost:8088/thinkerou/not-uuid
|
||||
```
|
||||
|
||||
### Bind Header
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type testHeader struct {
|
||||
Rate int `header:"Rate"`
|
||||
Domain string `header:"Domain"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
h := testHeader{}
|
||||
|
||||
if err := c.ShouldBindHeader(&h); err != nil {
|
||||
c.JSON(200, err)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", h)
|
||||
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
|
||||
})
|
||||
|
||||
r.Run()
|
||||
|
||||
// client
|
||||
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
|
||||
// output
|
||||
// {"Domain":"music","Rate":300}
|
||||
}
|
||||
```
|
||||
|
||||
### Bind HTML checkboxes
|
||||
|
||||
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
|
||||
|
@ -954,32 +1001,36 @@ result:
|
|||
### Multipart/Urlencoded binding
|
||||
|
||||
```go
|
||||
package main
|
||||
type ProfileForm struct {
|
||||
Name string `form:"name" binding:"required"`
|
||||
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LoginForm struct {
|
||||
User string `form:"user" binding:"required"`
|
||||
Password string `form:"password" binding:"required"`
|
||||
// or for multiple files
|
||||
// Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
router.POST("/login", func(c *gin.Context) {
|
||||
router.POST("/profile", func(c *gin.Context) {
|
||||
// you can bind multipart form with explicit binding declaration:
|
||||
// c.ShouldBindWith(&form, binding.Form)
|
||||
// or you can simply use autobinding with ShouldBind method:
|
||||
var form LoginForm
|
||||
var form ProfileForm
|
||||
// in this case proper binding will be automatically selected
|
||||
if c.ShouldBind(&form) == nil {
|
||||
if form.User == "user" && form.Password == "password" {
|
||||
c.JSON(200, gin.H{"status": "you are logged in"})
|
||||
} else {
|
||||
c.JSON(401, gin.H{"status": "unauthorized"})
|
||||
if err := c.ShouldBind(&form); err != nil {
|
||||
c.String(http.StatusBadRequest, "bad request")
|
||||
return
|
||||
}
|
||||
|
||||
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "unknown error")
|
||||
return
|
||||
}
|
||||
|
||||
// db.Save(&form)
|
||||
|
||||
c.String(http.StatusOK, "ok")
|
||||
})
|
||||
router.Run(":8080")
|
||||
}
|
||||
|
@ -987,7 +1038,7 @@ func main() {
|
|||
|
||||
Test it with:
|
||||
```sh
|
||||
$ curl -v --form user=user --form password=password http://localhost:8080/login
|
||||
$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
|
||||
```
|
||||
|
||||
### XML, JSON, YAML and ProtoBuf rendering
|
||||
|
@ -1072,8 +1123,8 @@ Using JSONP to request data from a server in a different domain. Add callback t
|
|||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/JSONP?callback=x", func(c *gin.Context) {
|
||||
data := map[string]interface{}{
|
||||
r.GET("/JSONP", func(c *gin.Context) {
|
||||
data := gin.H{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
|
@ -1084,6 +1135,9 @@ func main() {
|
|||
|
||||
// Listen and serve on 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
|
||||
// client
|
||||
// curl http://127.0.0.1:8080/JSONP?callback=x
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1096,7 +1150,7 @@ func main() {
|
|||
r := gin.Default()
|
||||
|
||||
r.GET("/someJSON", func(c *gin.Context) {
|
||||
data := map[string]interface{}{
|
||||
data := gin.H{
|
||||
"lang": "GO语言",
|
||||
"tag": "<br>",
|
||||
}
|
||||
|
@ -1305,7 +1359,7 @@ func main() {
|
|||
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
|
||||
|
||||
router.GET("/raw", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
|
||||
c.HTML(http.StatusOK, "raw.tmpl", gin.H{
|
||||
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
|
||||
})
|
||||
})
|
||||
|
@ -1694,7 +1748,7 @@ func main() {
|
|||
quit := make(chan os.Signal)
|
||||
// kill (no param) default send syscall.SIGTERM
|
||||
// kill -2 is syscall.SIGINT
|
||||
// kill -9 is syscall.SIGKILL but can"t be catch, so don't need add it
|
||||
// kill -9 is syscall.SIGKILL but can't be catch, so don't need add it
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutdown Server ...")
|
||||
|
@ -2064,3 +2118,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
|
|||
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
|
||||
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
|
||||
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
|
||||
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.
|
||||
|
|
|
@ -78,6 +78,7 @@ var (
|
|||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
|
|
|
@ -67,6 +67,13 @@ type FooStructUseNumber struct {
|
|||
type FooBarStructForTimeType struct {
|
||||
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
|
||||
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
}
|
||||
|
||||
type FooStructForTimeTypeNotUnixFormat struct {
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
}
|
||||
|
||||
type FooStructForTimeTypeNotFormat struct {
|
||||
|
@ -226,7 +233,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
|
|||
func TestBindingFormForTime(t *testing.T) {
|
||||
testFormBindingForTime(t, "POST",
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&time_bar=", "bar2=foo")
|
||||
"time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
|
||||
testFormBindingForTimeNotUnixFormat(t, "POST",
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||
testFormBindingForTimeNotFormat(t, "POST",
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15", "bar2=foo")
|
||||
|
@ -240,8 +250,11 @@ func TestBindingFormForTime(t *testing.T) {
|
|||
|
||||
func TestBindingFormForTime2(t *testing.T) {
|
||||
testFormBindingForTime(t, "GET",
|
||||
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo",
|
||||
"/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
|
||||
"", "")
|
||||
testFormBindingForTimeNotUnixFormat(t, "POST",
|
||||
"/", "/",
|
||||
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
|
||||
testFormBindingForTimeNotFormat(t, "GET",
|
||||
"/?time_foo=2017-11-15", "/?bar2=foo",
|
||||
"", "")
|
||||
|
@ -667,6 +680,31 @@ func TestExistsFails(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestHeaderBinding(t *testing.T) {
|
||||
h := Header
|
||||
assert.Equal(t, "header", h.Name())
|
||||
|
||||
type tHeader struct {
|
||||
Limit int `header:"limit"`
|
||||
}
|
||||
|
||||
var theader tHeader
|
||||
req := requestWithBody("GET", "/", "")
|
||||
req.Header.Add("limit", "1000")
|
||||
assert.NoError(t, h.Bind(req, &theader))
|
||||
assert.Equal(t, 1000, theader.Limit)
|
||||
|
||||
req = requestWithBody("GET", "/", "")
|
||||
req.Header.Add("fail", `{fail:fail}`)
|
||||
|
||||
type failStruct struct {
|
||||
Fail map[string]interface{} `header:"fail"`
|
||||
}
|
||||
|
||||
err := h.Bind(req, &failStruct{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestUriBinding(t *testing.T) {
|
||||
b := Uri
|
||||
assert.Equal(t, "uri", b.Name())
|
||||
|
@ -824,6 +862,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
|||
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
|
||||
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
|
||||
assert.Equal(t, "UTC", obj.TimeBar.Location().String())
|
||||
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
|
||||
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
|
||||
|
||||
obj = FooBarStructForTimeType{}
|
||||
req = requestWithBody(method, badPath, badBody)
|
||||
|
@ -831,6 +871,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||
b := Form
|
||||
assert.Equal(t, "form", b.Name())
|
||||
|
||||
obj := FooStructForTimeTypeNotUnixFormat{}
|
||||
req := requestWithBody(method, path, body)
|
||||
if method == "POST" {
|
||||
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||
}
|
||||
err := b.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
|
||||
obj = FooStructForTimeTypeNotUnixFormat{}
|
||||
req = requestWithBody(method, badPath, badBody)
|
||||
err = JSON.Bind(req, &obj)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||
b := Form
|
||||
assert.Equal(t, "form", b.Name())
|
||||
|
|
|
@ -5,9 +5,7 @@
|
|||
package binding
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const defaultMemory = 32 * 1024 * 1024
|
||||
|
@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
|
|||
|
||||
return validate(obj)
|
||||
}
|
||||
|
||||
type multipartRequest http.Request
|
||||
|
||||
var _ setter = (*multipartRequest)(nil)
|
||||
|
||||
var (
|
||||
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
|
||||
)
|
||||
|
||||
// TrySet tries to set a value by the multipart request with the binding a form file
|
||||
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
|
||||
if value.Type() == multipartFileHeaderStructType {
|
||||
_, file, err := (*http.Request)(r).FormFile(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if file != nil {
|
||||
value.Set(reflect.ValueOf(*file))
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return setByForm(value, field, r.MultipartForm.Value, key, opt)
|
||||
}
|
||||
|
|
|
@ -126,9 +126,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||
for len(opts) > 0 {
|
||||
opt, opts = head(opts, ",")
|
||||
|
||||
k, v := head(opt, "=")
|
||||
switch k {
|
||||
case "default":
|
||||
if k, v := head(opt, "="); k == "default" {
|
||||
setOpt.isDefaultExists = true
|
||||
setOpt.defaultValue = v
|
||||
}
|
||||
|
@ -268,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||
timeFormat = time.RFC3339
|
||||
}
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d := time.Duration(1)
|
||||
if tf == "unixnano" {
|
||||
d = time.Second
|
||||
}
|
||||
|
||||
t := time.Unix(tv/int64(d), tv%int64(d))
|
||||
value.Set(reflect.ValueOf(t))
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
if val == "" {
|
||||
value.Set(reflect.ValueOf(time.Time{}))
|
||||
return nil
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package binding
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type headerBinding struct{}
|
||||
|
||||
func (headerBinding) Name() string {
|
||||
return "header"
|
||||
}
|
||||
|
||||
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
|
||||
|
||||
if err := mapHeader(obj, req.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return validate(obj)
|
||||
}
|
||||
|
||||
func mapHeader(ptr interface{}, h map[string][]string) error {
|
||||
return mappingByPtr(ptr, headerSource(h), "header")
|
||||
}
|
||||
|
||||
type headerSource map[string][]string
|
||||
|
||||
var _ setter = headerSource(nil)
|
||||
|
||||
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
|
||||
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2019 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type multipartRequest http.Request
|
||||
|
||||
var _ setter = (*multipartRequest)(nil)
|
||||
|
||||
// TrySet tries to set a value by the multipart request with the binding a form file
|
||||
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
|
||||
if files := r.MultipartForm.File[key]; len(files) != 0 {
|
||||
return setByMultipartFormFile(value, field, files)
|
||||
}
|
||||
|
||||
return setByForm(value, field, r.MultipartForm.Value, key, opt)
|
||||
}
|
||||
|
||||
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr:
|
||||
switch value.Interface().(type) {
|
||||
case *multipart.FileHeader:
|
||||
value.Set(reflect.ValueOf(files[0]))
|
||||
return true, nil
|
||||
}
|
||||
case reflect.Struct:
|
||||
switch value.Interface().(type) {
|
||||
case multipart.FileHeader:
|
||||
value.Set(reflect.ValueOf(*files[0]))
|
||||
return true, nil
|
||||
}
|
||||
case reflect.Slice:
|
||||
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
|
||||
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
|
||||
if err != nil || !isSetted {
|
||||
return isSetted, err
|
||||
}
|
||||
value.Set(slice)
|
||||
return true, nil
|
||||
case reflect.Array:
|
||||
return setArrayOfMultipartFormFiles(value, field, files)
|
||||
}
|
||||
return false, errors.New("unsupported field type for multipart.FileHeader")
|
||||
}
|
||||
|
||||
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
|
||||
if value.Len() != len(files) {
|
||||
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
|
||||
}
|
||||
for i := range files {
|
||||
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
|
||||
if err != nil || !setted {
|
||||
return setted, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright 2019 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFormMultipartBindingBindOneFile(t *testing.T) {
|
||||
var s struct {
|
||||
FileValue multipart.FileHeader `form:"file"`
|
||||
FilePtr *multipart.FileHeader `form:"file"`
|
||||
SliceValues []multipart.FileHeader `form:"file"`
|
||||
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||
ArrayValues [1]multipart.FileHeader `form:"file"`
|
||||
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
|
||||
}
|
||||
file := testFile{"file", "file1", []byte("hello")}
|
||||
|
||||
req := createRequestMultipartFiles(t, file)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assertMultipartFileHeader(t, &s.FileValue, file)
|
||||
assertMultipartFileHeader(t, s.FilePtr, file)
|
||||
assert.Len(t, s.SliceValues, 1)
|
||||
assertMultipartFileHeader(t, &s.SliceValues[0], file)
|
||||
assert.Len(t, s.SlicePtrs, 1)
|
||||
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
|
||||
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
|
||||
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
|
||||
}
|
||||
|
||||
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
|
||||
var s struct {
|
||||
SliceValues []multipart.FileHeader `form:"file"`
|
||||
SlicePtrs []*multipart.FileHeader `form:"file"`
|
||||
ArrayValues [2]multipart.FileHeader `form:"file"`
|
||||
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
|
||||
}
|
||||
files := []testFile{
|
||||
{"file", "file1", []byte("hello")},
|
||||
{"file", "file2", []byte("world")},
|
||||
}
|
||||
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, &s)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, s.SliceValues, len(files))
|
||||
assert.Len(t, s.SlicePtrs, len(files))
|
||||
assert.Len(t, s.ArrayValues, len(files))
|
||||
assert.Len(t, s.ArrayPtrs, len(files))
|
||||
|
||||
for i, file := range files {
|
||||
assertMultipartFileHeader(t, &s.SliceValues[i], file)
|
||||
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
|
||||
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
|
||||
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormMultipartBindingBindError(t *testing.T) {
|
||||
files := []testFile{
|
||||
{"file", "file1", []byte("hello")},
|
||||
{"file", "file2", []byte("world")},
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
s interface{}
|
||||
}{
|
||||
{"wrong type", &struct {
|
||||
Files int `form:"file"`
|
||||
}{}},
|
||||
{"wrong array size", &struct {
|
||||
Files [1]*multipart.FileHeader `form:"file"`
|
||||
}{}},
|
||||
{"wrong slice type", &struct {
|
||||
Files []int `form:"file"`
|
||||
}{}},
|
||||
} {
|
||||
req := createRequestMultipartFiles(t, files...)
|
||||
err := FormMultipart.Bind(req, tt.s)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
type testFile struct {
|
||||
Fieldname string
|
||||
Filename string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
|
||||
var body bytes.Buffer
|
||||
|
||||
mw := multipart.NewWriter(&body)
|
||||
for _, file := range files {
|
||||
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
n, err := fw.Write(file.Content)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(file.Content), n)
|
||||
}
|
||||
err := mw.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest("POST", "/", &body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
|
||||
return req
|
||||
}
|
||||
|
||||
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
|
||||
assert.Equal(t, file.Filename, fh.Filename)
|
||||
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
|
||||
|
||||
fl, err := fh.Open()
|
||||
assert.NoError(t, err)
|
||||
|
||||
body, err := ioutil.ReadAll(fl)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(file.Content), string(body))
|
||||
|
||||
err = fl.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
64
context.go
64
context.go
|
@ -51,6 +51,7 @@ type Context struct {
|
|||
Params Params
|
||||
handlers HandlersChain
|
||||
index int8
|
||||
fullPath string
|
||||
|
||||
engine *Engine
|
||||
|
||||
|
@ -62,6 +63,13 @@ type Context struct {
|
|||
|
||||
// Accepted defines a list of manually accepted formats for content negotiation.
|
||||
Accepted []string
|
||||
|
||||
// queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query()
|
||||
queryCache url.Values
|
||||
|
||||
// formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH,
|
||||
// or PUT body parameters.
|
||||
formCache url.Values
|
||||
}
|
||||
|
||||
/************************************/
|
||||
|
@ -73,9 +81,12 @@ func (c *Context) reset() {
|
|||
c.Params = c.Params[0:0]
|
||||
c.handlers = nil
|
||||
c.index = -1
|
||||
c.fullPath = ""
|
||||
c.Keys = nil
|
||||
c.Errors = c.Errors[0:0]
|
||||
c.Accepted = nil
|
||||
c.queryCache = nil
|
||||
c.formCache = nil
|
||||
}
|
||||
|
||||
type gin_context string
|
||||
|
@ -121,6 +132,9 @@ func (c *Context) Copy() *Context {
|
|||
for k, v := range c.Keys {
|
||||
cp.Keys[k] = v
|
||||
}
|
||||
paramCopy := make([]Param, len(cp.Params))
|
||||
copy(paramCopy, cp.Params)
|
||||
cp.Params = paramCopy
|
||||
return &cp
|
||||
}
|
||||
|
||||
|
@ -145,6 +159,15 @@ func (c *Context) Handler() HandlerFunc {
|
|||
return c.handlers.Last()
|
||||
}
|
||||
|
||||
// FullPath returns a matched route full path. For not found routes
|
||||
// returns an empty string.
|
||||
// router.GET("/user/:id", func(c *gin.Context) {
|
||||
// c.FullPath() == "/user/:id" // true
|
||||
// })
|
||||
func (c *Context) FullPath() string {
|
||||
return c.fullPath
|
||||
}
|
||||
|
||||
/************************************/
|
||||
/*********** FLOW CONTROL ***********/
|
||||
/************************************/
|
||||
|
@ -402,10 +425,18 @@ func (c *Context) QueryArray(key string) []string {
|
|||
return values
|
||||
}
|
||||
|
||||
func (c *Context) getQueryCache() {
|
||||
if c.queryCache == nil {
|
||||
c.queryCache = make(url.Values)
|
||||
c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueryArray returns a slice of strings for a given query key, plus
|
||||
// a boolean value whether at least one value exists for the given key.
|
||||
func (c *Context) GetQueryArray(key string) ([]string, bool) {
|
||||
if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 {
|
||||
c.getQueryCache()
|
||||
if values, ok := c.queryCache[key]; ok && len(values) > 0 {
|
||||
return values, true
|
||||
}
|
||||
return []string{}, false
|
||||
|
@ -420,7 +451,8 @@ func (c *Context) QueryMap(key string) map[string]string {
|
|||
// GetQueryMap returns a map for a given query key, plus a boolean value
|
||||
// whether at least one value exists for the given key.
|
||||
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
|
||||
return c.get(c.Request.URL.Query(), key)
|
||||
c.getQueryCache()
|
||||
return c.get(c.queryCache, key)
|
||||
}
|
||||
|
||||
// PostForm returns the specified key from a POST urlencoded form or multipart form
|
||||
|
@ -461,16 +493,24 @@ func (c *Context) PostFormArray(key string) []string {
|
|||
return values
|
||||
}
|
||||
|
||||
// GetPostFormArray returns a slice of strings for a given form key, plus
|
||||
// a boolean value whether at least one value exists for the given key.
|
||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||
func (c *Context) getFormCache() {
|
||||
if c.formCache == nil {
|
||||
c.formCache = make(url.Values)
|
||||
req := c.Request
|
||||
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
|
||||
if err != http.ErrNotMultipart {
|
||||
debugPrint("error on parse multipart form array: %v", err)
|
||||
}
|
||||
}
|
||||
if values := req.PostForm[key]; len(values) > 0 {
|
||||
c.formCache = req.PostForm
|
||||
}
|
||||
}
|
||||
|
||||
// GetPostFormArray returns a slice of strings for a given form key, plus
|
||||
// a boolean value whether at least one value exists for the given key.
|
||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||
c.getFormCache()
|
||||
if values := c.formCache[key]; len(values) > 0 {
|
||||
return values, true
|
||||
}
|
||||
return []string{}, false
|
||||
|
@ -577,6 +617,11 @@ func (c *Context) BindYAML(obj interface{}) error {
|
|||
return c.MustBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
|
||||
func (c *Context) BindHeader(obj interface{}) error {
|
||||
return c.MustBindWith(obj, binding.Header)
|
||||
}
|
||||
|
||||
// BindUri binds the passed struct pointer using binding.Uri.
|
||||
// It will abort the request with HTTP 400 if any error occurs.
|
||||
func (c *Context) BindUri(obj interface{}) error {
|
||||
|
@ -631,6 +676,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
|
|||
return c.ShouldBindWith(obj, binding.YAML)
|
||||
}
|
||||
|
||||
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
|
||||
func (c *Context) ShouldBindHeader(obj interface{}) error {
|
||||
return c.ShouldBindWith(obj, binding.Header)
|
||||
}
|
||||
|
||||
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
|
||||
func (c *Context) ShouldBindUri(obj interface{}) error {
|
||||
m := make(map[string][]string)
|
||||
|
@ -705,7 +755,7 @@ func (c *Context) ContentType() string {
|
|||
// handshake is being initiated by the client.
|
||||
func (c *Context) IsWebsocket() bool {
|
||||
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
|
||||
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
|
||||
strings.EqualFold(c.requestHeader("Upgrade"), "websocket") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -6,6 +6,7 @@ package gin
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
@ -13,8 +14,10 @@ import (
|
|||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -22,7 +25,6 @@ import (
|
|||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
testdata "github.com/gin-gonic/gin/testdata/protoexample"
|
||||
)
|
||||
|
@ -660,7 +662,7 @@ func TestContextRenderJSON(t *testing.T) {
|
|||
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -688,7 +690,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
|
|||
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -714,7 +716,7 @@ func TestContextRenderAPIJSON(t *testing.T) {
|
|||
c.JSON(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -1117,7 +1119,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
|
|||
})
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
@ -1281,7 +1283,7 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
|
|||
_, err := buf.ReadFrom(w.Body)
|
||||
assert.NoError(t, err)
|
||||
jsonStringBody := buf.String()
|
||||
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
|
||||
assert.Equal(t, fmt.Sprint("{\"foo\":\"fooValue\",\"bar\":\"barValue\"}\n"), jsonStringBody)
|
||||
}
|
||||
|
||||
func TestContextError(t *testing.T) {
|
||||
|
@ -1434,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
|
|||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestContextBindHeader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||
c.Request.Header.Add("rate", "8000")
|
||||
c.Request.Header.Add("domain", "music")
|
||||
c.Request.Header.Add("limit", "1000")
|
||||
|
||||
var testHeader struct {
|
||||
Rate int `header:"Rate"`
|
||||
Domain string `header:"Domain"`
|
||||
Limit int `header:"limit"`
|
||||
}
|
||||
|
||||
assert.NoError(t, c.BindHeader(&testHeader))
|
||||
assert.Equal(t, 8000, testHeader.Rate)
|
||||
assert.Equal(t, "music", testHeader.Domain)
|
||||
assert.Equal(t, 1000, testHeader.Limit)
|
||||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestContextBindWithQuery(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
@ -1541,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
|
|||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestContextShouldBindHeader(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||
c.Request.Header.Add("rate", "8000")
|
||||
c.Request.Header.Add("domain", "music")
|
||||
c.Request.Header.Add("limit", "1000")
|
||||
|
||||
var testHeader struct {
|
||||
Rate int `header:"Rate"`
|
||||
Domain string `header:"Domain"`
|
||||
Limit int `header:"limit"`
|
||||
}
|
||||
|
||||
assert.NoError(t, c.ShouldBindHeader(&testHeader))
|
||||
assert.Equal(t, 8000, testHeader.Rate)
|
||||
assert.Equal(t, "music", testHeader.Domain)
|
||||
assert.Equal(t, 1000, testHeader.Limit)
|
||||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestContextShouldBindWithQuery(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
|
@ -1822,6 +1868,7 @@ func TestContextResetInHandler(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
func UserFunc2Mock(ctx context.Context, t *testing.T) {
|
||||
v, ok := ctx.Value("user_self_defined_value").(string)
|
||||
assert.Equal(t, true, ok)
|
||||
|
@ -1856,3 +1903,24 @@ func TestStandardContext(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
UserFunc3Mock(ctx, t)
|
||||
}
|
||||
|
||||
func TestRaceParamsContextCopy(t *testing.T) {
|
||||
DefaultWriter = os.Stdout
|
||||
router := Default()
|
||||
nameGroup := router.Group("/:name")
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
{
|
||||
nameGroup.GET("/api", func(c *Context) {
|
||||
go func(c *Context, param string) {
|
||||
defer wg.Done()
|
||||
// First assert must be executed after the second request
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
assert.Equal(t, c.Param("name"), param)
|
||||
}(c.Copy(), c.Param("name"))
|
||||
})
|
||||
}
|
||||
performRequest(router, "GET", "/name1/api")
|
||||
performRequest(router, "GET", "/name2/api")
|
||||
wg.Wait()
|
||||
}
|
||||
|
|
7
debug.go
7
debug.go
|
@ -5,7 +5,6 @@
|
|||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"runtime"
|
||||
|
@ -13,7 +12,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const ginSupportMinGoVer = 8
|
||||
const ginSupportMinGoVer = 10
|
||||
|
||||
// IsDebugging returns true if the framework is running in debug mode.
|
||||
// Use SetMode(gin.ReleaseMode) to disable debug mode.
|
||||
|
@ -38,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
|
|||
|
||||
func debugPrintLoadTemplate(tmpl *template.Template) {
|
||||
if IsDebugging() {
|
||||
var buf bytes.Buffer
|
||||
var buf strings.Builder
|
||||
for _, tmpl := range tmpl.Templates() {
|
||||
buf.WriteString("\t- ")
|
||||
buf.WriteString(tmpl.Name())
|
||||
|
@ -68,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
|
|||
|
||||
func debugPrintWARNINGDefault() {
|
||||
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.
|
||||
debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.
|
||||
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
|
|||
})
|
||||
m, e := getMinVer(runtime.Version())
|
||||
if e == nil && m <= ginSupportMinGoVer {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
} else {
|
||||
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
package gin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin/internal/json"
|
||||
)
|
||||
|
@ -158,7 +158,7 @@ func (a errorMsgs) String() string {
|
|||
if len(a) == 0 {
|
||||
return ""
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
var buffer strings.Builder
|
||||
for i, msg := range a {
|
||||
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
|
||||
if msg.Meta != nil {
|
||||
|
|
14
gin.go
14
gin.go
|
@ -252,6 +252,7 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
|
|||
root := engine.trees.get(method)
|
||||
if root == nil {
|
||||
root = new(node)
|
||||
root.fullPath = "/"
|
||||
engine.trees = append(engine.trees, methodTree{method: method, root: root})
|
||||
}
|
||||
root.addRoute(path, handlers)
|
||||
|
@ -382,16 +383,17 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||
}
|
||||
root := t[i].root
|
||||
// Find route in tree
|
||||
handlers, params, tsr := root.getValue(rPath, c.Params, unescape)
|
||||
if handlers != nil {
|
||||
c.handlers = handlers
|
||||
c.Params = params
|
||||
value := root.getValue(rPath, c.Params, unescape)
|
||||
if value.handlers != nil {
|
||||
c.handlers = value.handlers
|
||||
c.Params = value.params
|
||||
c.fullPath = value.fullPath
|
||||
c.Next()
|
||||
c.writermem.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
if httpMethod != "CONNECT" && rPath != "/" {
|
||||
if tsr && engine.RedirectTrailingSlash {
|
||||
if value.tsr && engine.RedirectTrailingSlash {
|
||||
redirectTrailingSlash(c)
|
||||
return
|
||||
}
|
||||
|
@ -407,7 +409,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||
if tree.method == httpMethod {
|
||||
continue
|
||||
}
|
||||
if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil {
|
||||
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
|
||||
c.handlers = engine.allNoMethod
|
||||
serveError(c, http.StatusMethodNotAllowed, default405Body)
|
||||
return
|
||||
|
|
6
go.mod
6
go.mod
|
@ -3,15 +3,15 @@ module github.com/gin-gonic/gin
|
|||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/golang/protobuf v1.3.1
|
||||
github.com/json-iterator/go v1.1.6
|
||||
github.com/mattn/go-isatty v0.0.7
|
||||
github.com/mattn/go-isatty v0.0.8
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/stretchr/testify v1.3.0
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43
|
||||
github.com/ugorji/go/codec v1.1.7
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect
|
||||
|
|
33
go.sum
33
go.sum
|
@ -1,45 +1,28 @@
|
|||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
|
||||
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA=
|
||||
github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b h1:lohp5blsw53GBXtLyLNaTXPXS9pJ1tiTw61ZHUoE9Qw=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||
|
|
|
@ -369,15 +369,15 @@ func TestErrorLogger(t *testing.T) {
|
|||
|
||||
w := performRequest(router, "GET", "/error")
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/abort")
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"error\":\"no authorized\"}\n", w.Body.String())
|
||||
|
||||
w = performRequest(router, "GET", "/print")
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}\n", w.Body.String())
|
||||
}
|
||||
|
||||
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||
|
|
|
@ -246,5 +246,5 @@ func TestMiddlewareWrite(t *testing.T) {
|
|||
w := performRequest(router, "GET", "/")
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
|
||||
}
|
||||
|
|
|
@ -68,11 +68,8 @@ func (r JSON) WriteContentType(w http.ResponseWriter) {
|
|||
// WriteJSON marshals the given interface object and writes it with custom ContentType.
|
||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(jsonBytes)
|
||||
encoder := json.NewEncoder(w)
|
||||
err := encoder.Encode(&obj)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestRenderJSON(t *testing.T) {
|
|||
err := (JSON{data}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
|
||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}\n", w.Body.String())
|
||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
|
|
|
@ -554,3 +554,43 @@ func TestRouteServeErrorWithWriteHeader(t *testing.T) {
|
|||
assert.Equal(t, 421, w.Code)
|
||||
assert.Equal(t, 0, w.Body.Len())
|
||||
}
|
||||
|
||||
func TestRouteContextHoldsFullPath(t *testing.T) {
|
||||
router := New()
|
||||
|
||||
// Test routes
|
||||
routes := []string{
|
||||
"/simple",
|
||||
"/project/:name",
|
||||
"/",
|
||||
"/news/home",
|
||||
"/news",
|
||||
"/simple-two/one",
|
||||
"/simple-two/one-two",
|
||||
"/project/:name/build/*params",
|
||||
"/project/:name/bui",
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
actualRoute := route
|
||||
router.GET(route, func(c *Context) {
|
||||
// For each defined route context should contain its full path
|
||||
assert.Equal(t, actualRoute, c.FullPath())
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
w := performRequest(router, "GET", route)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
// Test not found
|
||||
router.Use(func(c *Context) {
|
||||
// For not found routes full path is empty
|
||||
assert.Equal(t, "", c.FullPath())
|
||||
})
|
||||
|
||||
w := performRequest(router, "GET", "/not-found")
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
|
83
tree.go
83
tree.go
|
@ -94,6 +94,7 @@ type node struct {
|
|||
nType nodeType
|
||||
maxParams uint8
|
||||
wildChild bool
|
||||
fullPath string
|
||||
}
|
||||
|
||||
// increments priority of the given child and reorders if necessary.
|
||||
|
@ -127,6 +128,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
n.priority++
|
||||
numParams := countParams(path)
|
||||
|
||||
parentFullPathIndex := 0
|
||||
|
||||
// non-empty tree
|
||||
if len(n.path) > 0 || len(n.children) > 0 {
|
||||
walk:
|
||||
|
@ -154,6 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
children: n.children,
|
||||
handlers: n.handlers,
|
||||
priority: n.priority - 1,
|
||||
fullPath: n.fullPath,
|
||||
}
|
||||
|
||||
// Update maxParams (max of all children)
|
||||
|
@ -169,6 +173,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
n.path = path[:i]
|
||||
n.handlers = nil
|
||||
n.wildChild = false
|
||||
n.fullPath = fullPath[:parentFullPathIndex+i]
|
||||
}
|
||||
|
||||
// Make new node a child of this node
|
||||
|
@ -176,6 +181,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
path = path[i:]
|
||||
|
||||
if n.wildChild {
|
||||
parentFullPathIndex += len(n.path)
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
|
||||
|
@ -209,6 +215,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
|
||||
// slash after param
|
||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||
parentFullPathIndex += len(n.path)
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
continue walk
|
||||
|
@ -217,6 +224,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
// Check if a child with the next path byte exists
|
||||
for i := 0; i < len(n.indices); i++ {
|
||||
if c == n.indices[i] {
|
||||
parentFullPathIndex += len(n.path)
|
||||
i = n.incrementChildPrio(i)
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
|
@ -229,6 +237,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
|||
n.indices += string([]byte{c})
|
||||
child := &node{
|
||||
maxParams: numParams,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = append(n.children, child)
|
||||
n.incrementChildPrio(len(n.indices) - 1)
|
||||
|
@ -296,6 +305,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
child := &node{
|
||||
nType: param,
|
||||
maxParams: numParams,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.wildChild = true
|
||||
|
@ -312,6 +322,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
child := &node{
|
||||
maxParams: numParams,
|
||||
priority: 1,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n = child
|
||||
|
@ -339,6 +350,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
wildChild: true,
|
||||
nType: catchAll,
|
||||
maxParams: 1,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
n.indices = string(path[i])
|
||||
|
@ -352,6 +364,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
maxParams: 1,
|
||||
handlers: handlers,
|
||||
priority: 1,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
|
||||
|
@ -362,6 +375,15 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
// insert remaining path part and handle to the leaf
|
||||
n.path = path[offset:]
|
||||
n.handlers = handlers
|
||||
n.fullPath = fullPath
|
||||
}
|
||||
|
||||
// nodeValue holds return values of (*Node).getValue method
|
||||
type nodeValue struct {
|
||||
handlers HandlersChain
|
||||
params Params
|
||||
tsr bool
|
||||
fullPath string
|
||||
}
|
||||
|
||||
// getValue returns the handle registered with the given path (key). The values of
|
||||
|
@ -369,8 +391,8 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
|
|||
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||
// made if a handle exists with an extra (without the) trailing slash for the
|
||||
// given path.
|
||||
func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
|
||||
p = po
|
||||
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
|
||||
value.params = po
|
||||
walk: // Outer loop for walking the tree
|
||||
for {
|
||||
if len(path) > len(n.path) {
|
||||
|
@ -391,7 +413,7 @@ walk: // Outer loop for walking the tree
|
|||
// Nothing found.
|
||||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
tsr = path == "/" && n.handlers != nil
|
||||
value.tsr = path == "/" && n.handlers != nil
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -406,20 +428,20 @@ walk: // Outer loop for walking the tree
|
|||
}
|
||||
|
||||
// save param value
|
||||
if cap(p) < int(n.maxParams) {
|
||||
p = make(Params, 0, n.maxParams)
|
||||
if cap(value.params) < int(n.maxParams) {
|
||||
value.params = make(Params, 0, n.maxParams)
|
||||
}
|
||||
i := len(p)
|
||||
p = p[:i+1] // expand slice within preallocated capacity
|
||||
p[i].Key = n.path[1:]
|
||||
i := len(value.params)
|
||||
value.params = value.params[:i+1] // expand slice within preallocated capacity
|
||||
value.params[i].Key = n.path[1:]
|
||||
val := path[:end]
|
||||
if unescape {
|
||||
var err error
|
||||
if p[i].Value, err = url.QueryUnescape(val); err != nil {
|
||||
p[i].Value = val // fallback, in case of error
|
||||
if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
|
||||
value.params[i].Value = val // fallback, in case of error
|
||||
}
|
||||
} else {
|
||||
p[i].Value = val
|
||||
value.params[i].Value = val
|
||||
}
|
||||
|
||||
// we need to go deeper!
|
||||
|
@ -431,40 +453,42 @@ walk: // Outer loop for walking the tree
|
|||
}
|
||||
|
||||
// ... but we can't
|
||||
tsr = len(path) == end+1
|
||||
value.tsr = len(path) == end+1
|
||||
return
|
||||
}
|
||||
|
||||
if handlers = n.handlers; handlers != nil {
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
}
|
||||
if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for TSR recommendation
|
||||
n = n.children[0]
|
||||
tsr = n.path == "/" && n.handlers != nil
|
||||
value.tsr = n.path == "/" && n.handlers != nil
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
case catchAll:
|
||||
// save param value
|
||||
if cap(p) < int(n.maxParams) {
|
||||
p = make(Params, 0, n.maxParams)
|
||||
if cap(value.params) < int(n.maxParams) {
|
||||
value.params = make(Params, 0, n.maxParams)
|
||||
}
|
||||
i := len(p)
|
||||
p = p[:i+1] // expand slice within preallocated capacity
|
||||
p[i].Key = n.path[2:]
|
||||
i := len(value.params)
|
||||
value.params = value.params[:i+1] // expand slice within preallocated capacity
|
||||
value.params[i].Key = n.path[2:]
|
||||
if unescape {
|
||||
var err error
|
||||
if p[i].Value, err = url.QueryUnescape(path); err != nil {
|
||||
p[i].Value = path // fallback, in case of error
|
||||
if value.params[i].Value, err = url.QueryUnescape(path); err != nil {
|
||||
value.params[i].Value = path // fallback, in case of error
|
||||
}
|
||||
} else {
|
||||
p[i].Value = path
|
||||
value.params[i].Value = path
|
||||
}
|
||||
|
||||
handlers = n.handlers
|
||||
value.handlers = n.handlers
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
|
||||
default:
|
||||
|
@ -474,12 +498,13 @@ walk: // Outer loop for walking the tree
|
|||
} else if path == n.path {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if handlers = n.handlers; handlers != nil {
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
tsr = true
|
||||
value.tsr = true
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -488,7 +513,7 @@ walk: // Outer loop for walking the tree
|
|||
for i := 0; i < len(n.indices); i++ {
|
||||
if n.indices[i] == '/' {
|
||||
n = n.children[i]
|
||||
tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||
return
|
||||
}
|
||||
|
@ -499,7 +524,7 @@ walk: // Outer loop for walking the tree
|
|||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
tsr = (path == "/") ||
|
||||
value.tsr = (path == "/") ||
|
||||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
||||
path == n.path[:len(n.path)-1] && n.handlers != nil)
|
||||
return
|
||||
|
@ -514,7 +539,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||
ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory
|
||||
|
||||
// Outer loop for walking the tree
|
||||
for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) {
|
||||
for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) {
|
||||
path = path[len(n.path):]
|
||||
ciPath = append(ciPath, n.path...)
|
||||
|
||||
|
@ -618,7 +643,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
|
|||
return ciPath, true
|
||||
}
|
||||
if len(path)+1 == len(n.path) && n.path[len(path)] == '/' &&
|
||||
strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) &&
|
||||
strings.EqualFold(path, n.path[:len(path)]) &&
|
||||
n.handlers != nil {
|
||||
return append(ciPath, n.path...), true
|
||||
}
|
||||
|
|
26
tree_test.go
26
tree_test.go
|
@ -35,22 +35,22 @@ func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ..
|
|||
}
|
||||
|
||||
for _, request := range requests {
|
||||
handler, ps, _ := tree.getValue(request.path, nil, unescape)
|
||||
value := tree.getValue(request.path, nil, unescape)
|
||||
|
||||
if handler == nil {
|
||||
if value.handlers == nil {
|
||||
if !request.nilHandler {
|
||||
t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
|
||||
}
|
||||
} else if request.nilHandler {
|
||||
t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
|
||||
} else {
|
||||
handler[0](nil)
|
||||
value.handlers[0](nil)
|
||||
if fakeHandlerValue != request.route {
|
||||
t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route)
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(ps, request.ps) {
|
||||
if !reflect.DeepEqual(value.params, request.ps) {
|
||||
t.Errorf("Params mismatch for route '%s'", request.path)
|
||||
}
|
||||
}
|
||||
|
@ -454,10 +454,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||
"/doc/",
|
||||
}
|
||||
for _, route := range tsrRoutes {
|
||||
handler, _, tsr := tree.getValue(route, nil, false)
|
||||
if handler != nil {
|
||||
value := tree.getValue(route, nil, false)
|
||||
if value.handlers != nil {
|
||||
t.Fatalf("non-nil handler for TSR route '%s", route)
|
||||
} else if !tsr {
|
||||
} else if !value.tsr {
|
||||
t.Errorf("expected TSR recommendation for route '%s'", route)
|
||||
}
|
||||
}
|
||||
|
@ -471,10 +471,10 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
|
|||
"/api/world/abc",
|
||||
}
|
||||
for _, route := range noTsrRoutes {
|
||||
handler, _, tsr := tree.getValue(route, nil, false)
|
||||
if handler != nil {
|
||||
value := tree.getValue(route, nil, false)
|
||||
if value.handlers != nil {
|
||||
t.Fatalf("non-nil handler for No-TSR route '%s", route)
|
||||
} else if tsr {
|
||||
} else if value.tsr {
|
||||
t.Errorf("expected no TSR recommendation for route '%s'", route)
|
||||
}
|
||||
}
|
||||
|
@ -490,10 +490,10 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
|
|||
t.Fatalf("panic inserting test route: %v", recv)
|
||||
}
|
||||
|
||||
handler, _, tsr := tree.getValue("/", nil, false)
|
||||
if handler != nil {
|
||||
value := tree.getValue("/", nil, false)
|
||||
if value.handlers != nil {
|
||||
t.Fatalf("non-nil handler")
|
||||
} else if tsr {
|
||||
} else if value.tsr {
|
||||
t.Errorf("expected no TSR recommendation")
|
||||
}
|
||||
}
|
||||
|
|
2
utils.go
2
utils.go
|
@ -146,6 +146,6 @@ func resolveAddress(addr []string) string {
|
|||
case 1:
|
||||
return addr[0]
|
||||
default:
|
||||
panic("too much parameters")
|
||||
panic("too many parameters")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
"versionExact": "v1.1.1"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
|
||||
"checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
|
||||
"path": "github.com/gin-contrib/sse",
|
||||
"revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae",
|
||||
"revisionTime": "2019-03-01T06:25:29Z"
|
||||
"revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
|
||||
"revisionTime": "2019-06-02T15:02:53Z",
|
||||
"version": "v0.1",
|
||||
"versionExact": "v0.1.0"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
|
||||
|
@ -81,18 +83,12 @@
|
|||
"versionExact": "v1.2.2"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=",
|
||||
"checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
|
||||
"path": "github.com/ugorji/go/codec",
|
||||
"revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13",
|
||||
"revisionTime": "2019-04-08T19:08:48Z",
|
||||
"revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
|
||||
"revisionTime": "2019-07-02T14:15:27Z",
|
||||
"version": "v1.1",
|
||||
"versionExact": "v1.1.4"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
|
||||
"path": "golang.org/x/net/context",
|
||||
"revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7",
|
||||
"revisionTime": "2019-05-02T22:26:14Z"
|
||||
"versionExact": "v1.1.6"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
|
||||
|
|
Loading…
Reference in New Issue