Browse Source

Merge branch 'master' into thinkerou-patch-1

Bo-Yi Wu 6 years ago
parent
commit
7f60516064
14 changed files with 404 additions and 56 deletions
  1. 0 2
      .travis.yml
  2. 68 23
      README.md
  3. 1 0
      binding/binding.go
  4. 25 0
      binding/binding_test.go
  5. 0 26
      binding/form.go
  6. 34 0
      binding/header.go
  7. 66 0
      binding/multipart_form_mapping.go
  8. 138 0
      binding/multipart_form_mapping_test.go
  9. 11 0
      context.go
  10. 44 0
      context_test.go
  11. 2 2
      debug.go
  12. 1 1
      debug_test.go
  13. 6 1
      routes_test.go
  14. 8 1
      tree.go

+ 0 - 2
.travis.yml

@@ -3,8 +3,6 @@ language: go
 matrix:
 matrix:
   fast_finish: true
   fast_finish: true
   include:
   include:
-  - go: 1.8.x
-  - go: 1.9.x
   - go: 1.10.x
   - go: 1.10.x
   - go: 1.11.x
   - go: 1.11.x
     env: GO111MODULE=on
     env: GO111MODULE=on

+ 68 - 23
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)
     - [Only Bind Query String](#only-bind-query-string)
     - [Bind Query String or Post Data](#bind-query-string-or-post-data)
     - [Bind Query String or Post Data](#bind-query-string-or-post-data)
     - [Bind Uri](#bind-uri)
     - [Bind Uri](#bind-uri)
+    - [Bind Header](#bind-header)
     - [Bind HTML checkboxes](#bind-html-checkboxes)
     - [Bind HTML checkboxes](#bind-html-checkboxes)
     - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
     - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
     - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
     - [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.
 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
 ```sh
 $ go get -u github.com/gin-gonic/gin
 $ go get -u github.com/gin-gonic/gin
@@ -755,7 +756,7 @@ func bookableDate(
 ) bool {
 ) bool {
 	if date, ok := field.Interface().(time.Time); ok {
 	if date, ok := field.Interface().(time.Time); ok {
 		today := time.Now()
 		today := time.Now()
-		if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
+		if today.After(date) {
 			return false
 			return false
 		}
 		}
 	}
 	}
@@ -910,6 +911,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
 $ curl -v localhost:8088/thinkerou/not-uuid
 $ 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
 ### Bind HTML checkboxes
 
 
 See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
 See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@@ -959,32 +997,36 @@ result:
 ### Multipart/Urlencoded binding
 ### Multipart/Urlencoded binding
 
 
 ```go
 ```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() {
 func main() {
 	router := gin.Default()
 	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:
 		// you can bind multipart form with explicit binding declaration:
 		// c.ShouldBindWith(&form, binding.Form)
 		// c.ShouldBindWith(&form, binding.Form)
 		// or you can simply use autobinding with ShouldBind method:
 		// or you can simply use autobinding with ShouldBind method:
-		var form LoginForm
+		var form ProfileForm
 		// in this case proper binding will be automatically selected
 		// 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")
 	router.Run(":8080")
 }
 }
@@ -992,7 +1034,7 @@ func main() {
 
 
 Test it with:
 Test it with:
 ```sh
 ```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
 ### XML, JSON, YAML and ProtoBuf rendering
@@ -1077,8 +1119,8 @@ Using JSONP to request data from a server  in a different domain. Add callback t
 func main() {
 func main() {
 	r := gin.Default()
 	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",
 			"foo": "bar",
 		}
 		}
 		
 		
@@ -1089,6 +1131,9 @@ func main() {
 
 
 	// Listen and serve on 0.0.0.0:8080
 	// Listen and serve on 0.0.0.0:8080
 	r.Run(":8080")
 	r.Run(":8080")
+
+        // client
+        // curl http://127.0.0.1:8080/JSONP?callback=x
 }
 }
 ```
 ```
 
 
@@ -1101,7 +1146,7 @@ func main() {
 	r := gin.Default()
 	r := gin.Default()
 
 
 	r.GET("/someJSON", func(c *gin.Context) {
 	r.GET("/someJSON", func(c *gin.Context) {
-		data := map[string]interface{}{
+		data := gin.H{
 			"lang": "GO语言",
 			"lang": "GO语言",
 			"tag":  "<br>",
 			"tag":  "<br>",
 		}
 		}
@@ -1310,7 +1355,7 @@ func main() {
     router.LoadHTMLFiles("./testdata/template/raw.tmpl")
     router.LoadHTMLFiles("./testdata/template/raw.tmpl")
 
 
     router.GET("/raw", func(c *gin.Context) {
     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),
             "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
         })
         })
     })
     })

+ 1 - 0
binding/binding.go

@@ -78,6 +78,7 @@ var (
 	MsgPack       = msgpackBinding{}
 	MsgPack       = msgpackBinding{}
 	YAML          = yamlBinding{}
 	YAML          = yamlBinding{}
 	Uri           = uriBinding{}
 	Uri           = uriBinding{}
+	Header        = headerBinding{}
 )
 )
 
 
 // Default returns the appropriate Binding instance based on the HTTP method
 // Default returns the appropriate Binding instance based on the HTTP method

+ 25 - 0
binding/binding_test.go

@@ -667,6 +667,31 @@ func TestExistsFails(t *testing.T) {
 	assert.Error(t, err)
 	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) {
 func TestUriBinding(t *testing.T) {
 	b := Uri
 	b := Uri
 	assert.Equal(t, "uri", b.Name())
 	assert.Equal(t, "uri", b.Name())

+ 0 - 26
binding/form.go

@@ -5,9 +5,7 @@
 package binding
 package binding
 
 
 import (
 import (
-	"mime/multipart"
 	"net/http"
 	"net/http"
-	"reflect"
 )
 )
 
 
 const defaultMemory = 32 * 1024 * 1024
 const defaultMemory = 32 * 1024 * 1024
@@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
 
 
 	return validate(obj)
 	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)
-}

+ 34 - 0
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)
+}

+ 66 - 0
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
+}

+ 138 - 0
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)
+}

+ 11 - 0
context.go

@@ -83,6 +83,7 @@ func (c *Context) reset() {
 	c.Errors = c.Errors[0:0]
 	c.Errors = c.Errors[0:0]
 	c.Accepted = nil
 	c.Accepted = nil
 	c.queryCache = 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.
 // Copy returns a copy of the current context that can be safely used outside the request's scope.
@@ -582,6 +583,11 @@ func (c *Context) BindYAML(obj interface{}) error {
 	return c.MustBindWith(obj, binding.YAML)
 	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.
 // BindUri binds the passed struct pointer using binding.Uri.
 // It will abort the request with HTTP 400 if any error occurs.
 // It will abort the request with HTTP 400 if any error occurs.
 func (c *Context) BindUri(obj interface{}) error {
 func (c *Context) BindUri(obj interface{}) error {
@@ -636,6 +642,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
 	return c.ShouldBindWith(obj, binding.YAML)
 	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.
 // ShouldBindUri binds the passed struct pointer using the specified binding engine.
 func (c *Context) ShouldBindUri(obj interface{}) error {
 func (c *Context) ShouldBindUri(obj interface{}) error {
 	m := make(map[string][]string)
 	m := make(map[string][]string)

+ 44 - 0
context_test.go

@@ -1436,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
 	assert.Equal(t, 0, w.Body.Len())
 	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) {
 func TestContextBindWithQuery(t *testing.T) {
 	w := httptest.NewRecorder()
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 	c, _ := CreateTestContext(w)
@@ -1543,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
 	assert.Equal(t, 0, w.Body.Len())
 	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) {
 func TestContextShouldBindWithQuery(t *testing.T) {
 	w := httptest.NewRecorder()
 	w := httptest.NewRecorder()
 	c, _ := CreateTestContext(w)
 	c, _ := CreateTestContext(w)

+ 2 - 2
debug.go

@@ -13,7 +13,7 @@ import (
 	"strings"
 	"strings"
 )
 )
 
 
-const ginSupportMinGoVer = 8
+const ginSupportMinGoVer = 10
 
 
 // IsDebugging returns true if the framework is running in debug mode.
 // IsDebugging returns true if the framework is running in debug mode.
 // Use SetMode(gin.ReleaseMode) to disable debug mode.
 // Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -68,7 +68,7 @@ func getMinVer(v string) (uint64, error) {
 
 
 func debugPrintWARNINGDefault() {
 func debugPrintWARNINGDefault() {
 	if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
 	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.
 
 
 `)
 `)
 	}
 	}

+ 1 - 1
debug_test.go

@@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
 	})
 	})
 	m, e := getMinVer(runtime.Version())
 	m, e := getMinVer(runtime.Version())
 	if e == nil && m <= ginSupportMinGoVer {
 	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 {
 	} else {
 		assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
 		assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
 	}
 	}

+ 6 - 1
routes_test.go

@@ -560,10 +560,15 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
 
 
 	// Test routes
 	// Test routes
 	routes := []string{
 	routes := []string{
-		"/",
 		"/simple",
 		"/simple",
 		"/project/:name",
 		"/project/:name",
+		"/",
+		"/news/home",
+		"/news",
+		"/simple-two/one",
+		"/simple-two/one-two",
 		"/project/:name/build/*params",
 		"/project/:name/build/*params",
+		"/project/:name/bui",
 	}
 	}
 
 
 	for _, route := range routes {
 	for _, route := range routes {

+ 8 - 1
tree.go

@@ -128,6 +128,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 	n.priority++
 	n.priority++
 	numParams := countParams(path)
 	numParams := countParams(path)
 
 
+	parentFullPathIndex := 0
+
 	// non-empty tree
 	// non-empty tree
 	if len(n.path) > 0 || len(n.children) > 0 {
 	if len(n.path) > 0 || len(n.children) > 0 {
 	walk:
 	walk:
@@ -155,7 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 					children:  n.children,
 					children:  n.children,
 					handlers:  n.handlers,
 					handlers:  n.handlers,
 					priority:  n.priority - 1,
 					priority:  n.priority - 1,
-					fullPath:  fullPath,
+					fullPath:  n.fullPath,
 				}
 				}
 
 
 				// Update maxParams (max of all children)
 				// Update maxParams (max of all children)
@@ -171,6 +173,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 				n.path = path[:i]
 				n.path = path[:i]
 				n.handlers = nil
 				n.handlers = nil
 				n.wildChild = false
 				n.wildChild = false
+				n.fullPath = fullPath[:parentFullPathIndex+i]
 			}
 			}
 
 
 			// Make new node a child of this node
 			// Make new node a child of this node
@@ -178,6 +181,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 				path = path[i:]
 				path = path[i:]
 
 
 				if n.wildChild {
 				if n.wildChild {
+					parentFullPathIndex += len(n.path)
 					n = n.children[0]
 					n = n.children[0]
 					n.priority++
 					n.priority++
 
 
@@ -211,6 +215,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 
 
 				// slash after param
 				// slash after param
 				if n.nType == param && c == '/' && len(n.children) == 1 {
 				if n.nType == param && c == '/' && len(n.children) == 1 {
+					parentFullPathIndex += len(n.path)
 					n = n.children[0]
 					n = n.children[0]
 					n.priority++
 					n.priority++
 					continue walk
 					continue walk
@@ -219,6 +224,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
 				// Check if a child with the next path byte exists
 				// Check if a child with the next path byte exists
 				for i := 0; i < len(n.indices); i++ {
 				for i := 0; i < len(n.indices); i++ {
 					if c == n.indices[i] {
 					if c == n.indices[i] {
+						parentFullPathIndex += len(n.path)
 						i = n.incrementChildPrio(i)
 						i = n.incrementChildPrio(i)
 						n = n.children[i]
 						n = n.children[i]
 						continue walk
 						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
 	// insert remaining path part and handle to the leaf
 	n.path = path[offset:]
 	n.path = path[offset:]
 	n.handlers = handlers
 	n.handlers = handlers
+	n.fullPath = fullPath
 }
 }
 
 
 // nodeValue holds return values of (*Node).getValue method
 // nodeValue holds return values of (*Node).getValue method