Explorar el Código

support bind uri param (#1612)

* support bind uri (1)

* uri binding successful run

* fix vet warning: github.com/gin-gonic/gin/internal.Param composite literal uses unkeyed fields

* fix code style

* update function name

* fix test function signature

* add test for CanSet

* update readme and add test case

* remove internal.Params

* add coverage

* fix warning
thinkerou hace 7 años
padre
commit
521d06c81d
Se han modificado 8 ficheros con 158 adiciones y 31 borrados
  1. 35 0
      README.md
  2. 8 0
      binding/binding.go
  3. 30 0
      binding/binding_test.go
  4. 9 1
      binding/form_mapping.go
  5. 18 0
      binding/uri.go
  6. 10 3
      context.go
  7. 21 0
      githubapi_test.go
  8. 27 27
      tree_test.go

+ 35 - 0
README.md

@@ -39,6 +39,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
     - [Custom Validators](#custom-validators)
     - [Custom Validators](#custom-validators)
     - [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 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)
@@ -793,6 +794,40 @@ Test it with:
 $ 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"
 ```
 ```
 
 
+### Bind Uri
+
+See the [detail information](https://github.com/gin-gonic/gin/issues/846).
+
+```go
+package main
+
+import "github.com/gin-gonic/gin"
+
+type Person struct {
+	ID string `uri:"id" binding:"required,uuid"`
+	Name string `uri:"name" binding:"required"`
+}
+
+func main() {
+	route := gin.Default()
+	route.GET("/:name/:id", func(c *gin.Context) {
+		var person Person
+		if err := c.ShouldBindUri(&person); err != nil {
+			c.JSON(400, gin.H{"msg": err})
+			return
+		}
+		c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
+	})
+	route.Run(":8088")
+}
+```
+
+Test it with:
+```sh
+$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
+$ curl -v localhost:8088/thinkerou/not-uuid
+```
+
 ### 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)

+ 8 - 0
binding/binding.go

@@ -36,6 +36,13 @@ type BindingBody interface {
 	BindBody([]byte, interface{}) error
 	BindBody([]byte, interface{}) error
 }
 }
 
 
+// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
+// but it read the Params.
+type BindingUri interface {
+	Name() string
+	BindUri(map[string][]string, interface{}) error
+}
+
 // StructValidator is the minimal interface which needs to be implemented in
 // StructValidator is the minimal interface which needs to be implemented in
 // order for it to be used as the validator engine for ensuring the correctness
 // order for it to be used as the validator engine for ensuring the correctness
 // of the request. Gin provides a default implementation for this using
 // of the request. Gin provides a default implementation for this using
@@ -70,6 +77,7 @@ var (
 	ProtoBuf      = protobufBinding{}
 	ProtoBuf      = protobufBinding{}
 	MsgPack       = msgpackBinding{}
 	MsgPack       = msgpackBinding{}
 	YAML          = yamlBinding{}
 	YAML          = yamlBinding{}
+	Uri           = uriBinding{}
 )
 )
 
 
 // Default returns the appropriate Binding instance based on the HTTP method
 // Default returns the appropriate Binding instance based on the HTTP method

+ 30 - 0
binding/binding_test.go

@@ -662,6 +662,27 @@ func TestExistsFails(t *testing.T) {
 	assert.Error(t, err)
 	assert.Error(t, err)
 }
 }
 
 
+func TestUriBinding(t *testing.T) {
+	b := Uri
+	assert.Equal(t, "uri", b.Name())
+
+	type Tag struct {
+		Name string `uri:"name"`
+	}
+	var tag Tag
+	m := make(map[string][]string)
+	m["name"] = []string{"thinkerou"}
+	assert.NoError(t, b.BindUri(m, &tag))
+	assert.Equal(t, "thinkerou", tag.Name)
+
+	type NotSupportStruct struct {
+		Name map[string]interface{} `uri:"name"`
+	}
+	var not NotSupportStruct
+	assert.Error(t, b.BindUri(m, &not))
+	assert.Equal(t, "", not.Name)
+}
+
 func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
 func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
 	b := Form
 	b := Form
 	assert.Equal(t, "form", b.Name())
 	assert.Equal(t, "form", b.Name())
@@ -1232,3 +1253,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
 	req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
 	req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
 	return
 	return
 }
 }
+
+func TestCanSet(t *testing.T) {
+	type CanSetStruct struct {
+		lowerStart string `form:"lower"`
+	}
+
+	var c CanSetStruct
+	assert.Nil(t, mapForm(&c, nil))
+}

+ 9 - 1
binding/form_mapping.go

@@ -12,7 +12,15 @@ import (
 	"time"
 	"time"
 )
 )
 
 
+func mapUri(ptr interface{}, m map[string][]string) error {
+	return mapFormByTag(ptr, m, "uri")
+}
+
 func mapForm(ptr interface{}, form map[string][]string) error {
 func mapForm(ptr interface{}, form map[string][]string) error {
+	return mapFormByTag(ptr, form, "form")
+}
+
+func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
 	typ := reflect.TypeOf(ptr).Elem()
 	typ := reflect.TypeOf(ptr).Elem()
 	val := reflect.ValueOf(ptr).Elem()
 	val := reflect.ValueOf(ptr).Elem()
 	for i := 0; i < typ.NumField(); i++ {
 	for i := 0; i < typ.NumField(); i++ {
@@ -23,7 +31,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
 		}
 		}
 
 
 		structFieldKind := structField.Kind()
 		structFieldKind := structField.Kind()
-		inputFieldName := typeField.Tag.Get("form")
+		inputFieldName := typeField.Tag.Get(tag)
 		inputFieldNameList := strings.Split(inputFieldName, ",")
 		inputFieldNameList := strings.Split(inputFieldName, ",")
 		inputFieldName = inputFieldNameList[0]
 		inputFieldName = inputFieldNameList[0]
 		var defaultValue string
 		var defaultValue string

+ 18 - 0
binding/uri.go

@@ -0,0 +1,18 @@
+// Copyright 2018 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
+
+type uriBinding struct{}
+
+func (uriBinding) Name() string {
+	return "uri"
+}
+
+func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
+	if err := mapUri(obj, m); err != nil {
+		return err
+	}
+	return validate(obj)
+}

+ 10 - 3
context.go

@@ -574,6 +574,15 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
 	return c.ShouldBindWith(obj, binding.YAML)
 	return c.ShouldBindWith(obj, binding.YAML)
 }
 }
 
 
+// ShouldBindUri binds the passed struct pointer using the specified binding engine.
+func (c *Context) ShouldBindUri(obj interface{}) error {
+	m := make(map[string][]string)
+	for _, v := range c.Params {
+		m[v.Key] = []string{v.Value}
+	}
+	return binding.Uri.BindUri(m, obj)
+}
+
 // ShouldBindWith binds the passed struct pointer using the specified binding engine.
 // ShouldBindWith binds the passed struct pointer using the specified binding engine.
 // See the binding package.
 // See the binding package.
 func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
 func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
@@ -585,9 +594,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
 //
 //
 // NOTE: This method reads the body before binding. So you should use
 // NOTE: This method reads the body before binding. So you should use
 // ShouldBindWith for better performance if you need to call only once.
 // ShouldBindWith for better performance if you need to call only once.
-func (c *Context) ShouldBindBodyWith(
-	obj interface{}, bb binding.BindingBody,
-) (err error) {
+func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
 	var body []byte
 	var body []byte
 	if cb, ok := c.Get(BodyBytesKey); ok {
 	if cb, ok := c.Get(BodyBytesKey); ok {
 		if cbb, ok := cb.([]byte); ok {
 		if cbb, ok := cb.([]byte); ok {

+ 21 - 0
githubapi_test.go

@@ -285,6 +285,27 @@ var githubAPI = []route{
 	{"DELETE", "/user/keys/:id"},
 	{"DELETE", "/user/keys/:id"},
 }
 }
 
 
+func TestShouldBindUri(t *testing.T) {
+	DefaultWriter = os.Stdout
+	router := Default()
+
+	type Person struct {
+		Name string `uri:"name"`
+		Id   string `uri:"id"`
+	}
+	router.Handle("GET", "/rest/:name/:id", func(c *Context) {
+		var person Person
+		assert.NoError(t, c.ShouldBindUri(&person))
+		assert.True(t, "" != person.Name)
+		assert.True(t, "" != person.Id)
+		c.String(http.StatusOK, "ShouldBindUri test OK")
+	})
+
+	path, _ := exampleFromPath("/rest/:name/:id")
+	w := performRequest(router, "GET", path)
+	assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
+}
+
 func githubConfigRouter(router *Engine) {
 func githubConfigRouter(router *Engine) {
 	for _, route := range githubAPI {
 	for _, route := range githubAPI {
 		router.Handle(route.method, route.path, func(c *Context) {
 		router.Handle(route.method, route.path, func(c *Context) {

+ 27 - 27
tree_test.go

@@ -170,19 +170,19 @@ func TestTreeWildcard(t *testing.T) {
 
 
 	checkRequests(t, tree, testRequests{
 	checkRequests(t, tree, testRequests{
 		{"/", false, "/", nil},
 		{"/", false, "/", nil},
-		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
-		{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
-		{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
-		{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
-		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
+		{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
+		{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
+		{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
 		{"/search/", false, "/search/", nil},
 		{"/search/", false, "/search/", nil},
-		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
-		{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
-		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
-		{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
-		{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
-		{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
-		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
+		{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
+		{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
+		{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
+		{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
+		{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
+		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
 	})
 	})
 
 
 	checkPriorities(t, tree)
 	checkPriorities(t, tree)
@@ -209,18 +209,18 @@ func TestUnescapeParameters(t *testing.T) {
 	unescape := true
 	unescape := true
 	checkRequests(t, tree, testRequests{
 	checkRequests(t, tree, testRequests{
 		{"/", false, "/", nil},
 		{"/", false, "/", nil},
-		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
-		{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
-		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
-		{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
-		{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
-		{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
-		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
-		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
-		{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
-		{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
-		{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
-		{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
+		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
+		{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
+		{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
+		{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
+		{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
+		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
+		{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
+		{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
+		{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
+		{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
 	}, unescape)
 	}, unescape)
 
 
 	checkPriorities(t, tree)
 	checkPriorities(t, tree)
@@ -326,9 +326,9 @@ func TestTreeDupliatePath(t *testing.T) {
 	checkRequests(t, tree, testRequests{
 	checkRequests(t, tree, testRequests{
 		{"/", false, "/", nil},
 		{"/", false, "/", nil},
 		{"/doc/", false, "/doc/", nil},
 		{"/doc/", false, "/doc/", nil},
-		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
-		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
-		{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
+		{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
 	})
 	})
 }
 }