mirror of https://github.com/gin-gonic/gin.git
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
This commit is contained in:
parent
7ec82ee894
commit
521d06c81d
35
README.md
35
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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, ¬))
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
13
context.go
13
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(
|
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
|
||||||
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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
54
tree_test.go
54
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/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
|
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
|
||||||
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
|
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
{"/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é", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
|
||||||
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "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{"name", "gopher"}}},
|
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||||||
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "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{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
|
{"/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{"user", "gordon"}}},
|
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
|
||||||
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
|
{"/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/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
|
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
|
||||||
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
|
{"/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{"filepath", "/some/file test.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{"filepath", "/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{"filepath", "/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{"query", "someth!ng in ünìcodé"}}},
|
{"/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{"user", "gordon"}, Param{"project", "go"}}},
|
{"/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{"user", "slash/gordon"}}},
|
{"/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{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
|
{"/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{"user", "slash%%%%"}}},
|
{"/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{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
|
{"/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"}}},
|
{"/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{"query", "someth!ng+in+ünìcodé"}}},
|
{"/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{"name", "gopher"}}},
|
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue