mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-19 07:38:57 +08:00
Merge branch 'master' into upgrade_validator
This commit is contained in:
commit
7abcefaa79
@ -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)
|
||||
|
118
README.md
118
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
|
||||
@ -621,10 +622,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
|
||||
|
||||
Also, Gin provides two sets of methods for binding:
|
||||
- **Type** - Must bind
|
||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
|
||||
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader`
|
||||
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
|
||||
- **Type** - Should bind
|
||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
|
||||
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader`
|
||||
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
|
||||
|
||||
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
|
||||
@ -755,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
|
||||
}
|
||||
}
|
||||
@ -845,9 +846,11 @@ import (
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string `form:"name"`
|
||||
Address string `form:"address"`
|
||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||
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() {
|
||||
@ -861,11 +864,13 @@ func startPage(c *gin.Context) {
|
||||
// If `GET`, only `Form` binding engine (`query`) used.
|
||||
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
|
||||
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
||||
if c.ShouldBind(&person) == nil {
|
||||
log.Println(person.Name)
|
||||
log.Println(person.Address)
|
||||
log.Println(person.Birthday)
|
||||
}
|
||||
if c.ShouldBind(&person) == nil {
|
||||
log.Println(person.Name)
|
||||
log.Println(person.Address)
|
||||
log.Println(person.Birthday)
|
||||
log.Println(person.CreateTime)
|
||||
log.Println(person.UnixTime)
|
||||
}
|
||||
|
||||
c.String(200, "Success")
|
||||
}
|
||||
@ -873,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
|
||||
@ -910,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)
|
||||
@ -959,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")
|
||||
}
|
||||
@ -992,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
|
||||
@ -1077,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",
|
||||
}
|
||||
|
||||
@ -1089,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
|
||||
}
|
||||
```
|
||||
|
||||
@ -1101,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>",
|
||||
}
|
||||
@ -1310,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),
|
||||
})
|
||||
})
|
||||
@ -2069,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.
|
||||
|
9
auth.go
9
auth.go
@ -5,7 +5,6 @@
|
||||
package gin
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string {
|
||||
base := user + ":" + password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
|
||||
}
|
||||
|
||||
func secureCompare(given, actual string) bool {
|
||||
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
|
||||
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
|
||||
}
|
||||
// Securely compare actual to itself to keep constant time, but always return false.
|
||||
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
|
||||
}
|
||||
|
@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
|
||||
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
|
||||
}
|
||||
|
||||
func TestBasicAuthSecureCompare(t *testing.T) {
|
||||
assert.True(t, secureCompare("1234567890", "1234567890"))
|
||||
assert.False(t, secureCompare("123456789", "1234567890"))
|
||||
assert.False(t, secureCompare("12345678900", "1234567890"))
|
||||
assert.False(t, secureCompare("1234567891", "1234567890"))
|
||||
}
|
||||
|
||||
func TestBasicAuthSucceed(t *testing.T) {
|
||||
accounts := Accounts{"admin": "password"}
|
||||
router := New()
|
||||
|
@ -78,6 +78,7 @@ var (
|
||||
MsgPack = msgpackBinding{}
|
||||
YAML = yamlBinding{}
|
||||
Uri = uriBinding{}
|
||||
Header = headerBinding{}
|
||||
)
|
||||
|
||||
// Default returns the appropriate Binding instance based on the HTTP method
|
||||
|
@ -65,8 +65,15 @@ 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"`
|
||||
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 TestRequiredFails(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)
|
||||
}
|
||||
|
@ -266,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
|
||||
|
34
binding/header.go
Normal file
34
binding/header.go
Normal file
@ -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)
|
||||
}
|
66
binding/multipart_form_mapping.go
Normal file
66
binding/multipart_form_mapping.go
Normal file
@ -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
|
||||
}
|
138
binding/multipart_form_mapping_test.go
Normal file
138
binding/multipart_form_mapping_test.go
Normal file
@ -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)
|
||||
}
|
57
context.go
57
context.go
@ -60,6 +60,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
|
||||
}
|
||||
|
||||
/************************************/
|
||||
@ -75,6 +82,8 @@ func (c *Context) reset() {
|
||||
c.Keys = nil
|
||||
c.Errors = c.Errors[0:0]
|
||||
c.Accepted = nil
|
||||
c.queryCache = nil
|
||||
c.formCache = nil
|
||||
}
|
||||
|
||||
// Copy returns a copy of the current context that can be safely used outside the request's scope.
|
||||
@ -89,6 +98,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
|
||||
}
|
||||
|
||||
@ -379,10 +391,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
|
||||
@ -397,7 +417,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
|
||||
@ -438,16 +459,24 @@ func (c *Context) PostFormArray(key string) []string {
|
||||
return values
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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.getFormCache()
|
||||
if values := c.formCache[key]; len(values) > 0 {
|
||||
return values, true
|
||||
}
|
||||
return []string{}, false
|
||||
@ -554,6 +583,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 {
|
||||
@ -608,6 +642,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)
|
||||
|
@ -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"
|
||||
)
|
||||
@ -674,7 +676,7 @@ func TestContextRenderJSONP(t *testing.T) {
|
||||
c.JSONP(http.StatusCreated, H{"foo": "bar"})
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String())
|
||||
assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -1821,3 +1867,24 @@ func TestContextResetInHandler(t *testing.T) {
|
||||
c.Next()
|
||||
})
|
||||
}
|
||||
|
||||
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 {
|
||||
|
1
gin.go
1
gin.go
@ -437,7 +437,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
||||
return
|
||||
}
|
||||
c.writermem.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
|
||||
func redirectTrailingSlash(c *Context) {
|
||||
|
21
logger.go
21
logger.go
@ -22,18 +22,19 @@ const (
|
||||
forceColor
|
||||
)
|
||||
|
||||
var (
|
||||
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
|
||||
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
|
||||
yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
|
||||
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
|
||||
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
|
||||
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
|
||||
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
|
||||
reset = string([]byte{27, 91, 48, 109})
|
||||
consoleColorMode = autoColor
|
||||
const (
|
||||
green = "\033[97;42m"
|
||||
white = "\033[90;47m"
|
||||
yellow = "\033[90;43m"
|
||||
red = "\033[97;41m"
|
||||
blue = "\033[97;44m"
|
||||
magenta = "\033[97;45m"
|
||||
cyan = "\033[97;46m"
|
||||
reset = "\033[0m"
|
||||
)
|
||||
|
||||
var consoleColorMode = autoColor
|
||||
|
||||
// LoggerConfig defines the config for Logger middleware.
|
||||
type LoggerConfig struct {
|
||||
// Optional. Default value is gin.defaultLogFormatter
|
||||
|
@ -291,14 +291,14 @@ func TestColorForMethod(t *testing.T) {
|
||||
return p.MethodColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
|
||||
assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
||||
assert.Equal(t, blue, colorForMethod("GET"), "get should be blue")
|
||||
assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan")
|
||||
assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow")
|
||||
assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red")
|
||||
assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green")
|
||||
assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta")
|
||||
assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white")
|
||||
assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color")
|
||||
}
|
||||
|
||||
func TestColorForStatus(t *testing.T) {
|
||||
@ -309,10 +309,10 @@ func TestColorForStatus(t *testing.T) {
|
||||
return p.StatusCodeColor()
|
||||
}
|
||||
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
|
||||
assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green")
|
||||
assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||
assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||
assert.Equal(t, red, colorForStatus(2), "other things should be red")
|
||||
}
|
||||
|
||||
func TestResetColor(t *testing.T) {
|
||||
|
@ -138,7 +138,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write([]byte(")"))
|
||||
_, err = w.Write([]byte(");"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ type Reader struct {
|
||||
// Render (Reader) writes data with custom ContentType and headers.
|
||||
func (r Reader) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||
if r.ContentLength >= 0 {
|
||||
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
|
||||
}
|
||||
r.writeHeaders(w, r.Headers)
|
||||
_, err = io.Copy(w, r.Reader)
|
||||
return
|
||||
|
@ -45,7 +45,7 @@ func TestRenderMsgPack(t *testing.T) {
|
||||
err = codec.NewEncoder(buf, h).Encode(data)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, w.Body.String(), string(buf.Bytes()))
|
||||
assert.Equal(t, w.Body.String(), buf.String())
|
||||
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -146,7 +146,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
||||
err1 := (JsonpJSON{"x", data}).Render(w1)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String())
|
||||
assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
|
||||
|
||||
w2 := httptest.NewRecorder()
|
||||
@ -158,7 +158,7 @@ func TestRenderJsonpJSON(t *testing.T) {
|
||||
|
||||
err2 := (JsonpJSON{"x", datas}).Render(w2)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String())
|
||||
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String())
|
||||
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
@ -498,3 +498,26 @@ func TestRenderReader(t *testing.T) {
|
||||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||
}
|
||||
|
||||
func TestRenderReaderNoContentLength(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
body := "#!PNG some raw data"
|
||||
headers := make(map[string]string)
|
||||
headers["Content-Disposition"] = `attachment; filename="filename.png"`
|
||||
headers["x-request-id"] = "requestId"
|
||||
|
||||
err := (Reader{
|
||||
ContentLength: -1,
|
||||
ContentType: "image/png",
|
||||
Reader: strings.NewReader(body),
|
||||
Headers: headers,
|
||||
}).Render(w)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, body, w.Body.String())
|
||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||
assert.NotContains(t, "Content-Length", w.Header())
|
||||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||
}
|
||||
|
@ -560,10 +560,15 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
|
||||
|
||||
// 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 {
|
||||
|
9
tree.go
9
tree.go
@ -128,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:
|
||||
@ -155,7 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
|
||||
children: n.children,
|
||||
handlers: n.handlers,
|
||||
priority: n.priority - 1,
|
||||
fullPath: fullPath,
|
||||
fullPath: n.fullPath,
|
||||
}
|
||||
|
||||
// Update maxParams (max of all children)
|
||||
@ -171,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
|
||||
@ -178,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++
|
||||
|
||||
@ -211,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
|
||||
@ -219,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
|
||||
@ -369,6 +375,7 @@ 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
|
||||
|
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")
|
||||
}
|
||||
}
|
||||
|
22
vendor/vendor.json
vendored
22
vendor/vendor.json
vendored
@ -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": "b4DmyMT9bicTRVJw1hJXHLhIH+0=",
|
||||
@ -111,18 +113,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…
x
Reference in New Issue
Block a user