diff --git a/.travis.yml b/.travis.yml
index f6ec8a82..27c80ef8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ea2495d..15dfb1a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/README.md b/README.md
index 10fb1d45..2761259e 100644
--- a/README.md
+++ b/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
}
}
@@ -840,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() {
@@ -856,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")
}
@@ -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": "
",
}
@@ -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.
diff --git a/binding/binding.go b/binding/binding.go
index 520c5109..6d58c3cd 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -78,6 +78,7 @@ var (
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
+ Header = headerBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 6710e42b..806f3ac9 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -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 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())
diff --git a/binding/form.go b/binding/form.go
index 0b28aa8a..9e9fc3de 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -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)
-}
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 32c5b668..80b1d15a 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -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
diff --git a/binding/header.go b/binding/header.go
new file mode 100644
index 00000000..179ce4ea
--- /dev/null
+++ b/binding/header.go
@@ -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)
+}
diff --git a/binding/multipart_form_mapping.go b/binding/multipart_form_mapping.go
new file mode 100644
index 00000000..f85a1aa6
--- /dev/null
+++ b/binding/multipart_form_mapping.go
@@ -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
+}
diff --git a/binding/multipart_form_mapping_test.go b/binding/multipart_form_mapping_test.go
new file mode 100644
index 00000000..4c75d1fe
--- /dev/null
+++ b/binding/multipart_form_mapping_test.go
@@ -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)
+}
diff --git a/context.go b/context.go
index ca740459..2144cb25 100644
--- a/context.go
+++ b/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
}
+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
@@ -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
diff --git a/context_test.go b/context_test.go
index 08b9854b..3ec93521 100644
--- a/context_test.go
+++ b/context_test.go
@@ -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": ""})
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()
+}
diff --git a/debug.go b/debug.go
index 19e380fb..49080dbf 100644
--- a/debug.go
+++ b/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.
`)
}
diff --git a/debug_test.go b/debug_test.go
index 9ace2989..d6f320ef 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -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)
}
diff --git a/errors.go b/errors.go
index 6070ff55..25e8ff60 100644
--- a/errors.go
+++ b/errors.go
@@ -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 {
diff --git a/gin.go b/gin.go
index 8040f7b5..5b6ba5bf 100644
--- a/gin.go
+++ b/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
diff --git a/go.mod b/go.mod
index 4250681e..965d86d3 100644
--- a/go.mod
+++ b/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
diff --git a/go.sum b/go.sum
index d660e50c..fa1e8d03 100644
--- a/go.sum
+++ b/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=
@@ -47,4 +30,4 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
\ No newline at end of file
diff --git a/logger_test.go b/logger_test.go
index 56bb3a00..9177e1d9 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -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) {
diff --git a/middleware_test.go b/middleware_test.go
index fca1c530..2ae9e889 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -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{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
+ assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}
diff --git a/render/json.go b/render/json.go
index 18f27fa9..2b07cba0 100644
--- a/render/json.go
+++ b/render/json.go
@@ -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
}
diff --git a/render/render_test.go b/render/render_test.go
index 3aa5dbcc..9d7eaeef 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -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"))
}
diff --git a/routes_test.go b/routes_test.go
index e16c1376..0c2f9a0c 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -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)
+}
diff --git a/tree.go b/tree.go
index ada62ceb..371d5ad1 100644
--- a/tree.go
+++ b/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
}
diff --git a/tree_test.go b/tree_test.go
index dbb0352b..e6e28865 100644
--- a/tree_test.go
+++ b/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")
}
}
diff --git a/utils.go b/utils.go
index f4532d56..71b80de7 100644
--- a/utils.go
+++ b/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")
}
}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 4de0bfd1..fa8fd13a 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -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=",
@@ -118,4 +114,4 @@
}
],
"rootPath": "github.com/gin-gonic/gin"
-}
\ No newline at end of file
+}