Skip to content

Commit 521d06c

Browse files
thinkerouappleboy
authored andcommitted
support bind uri param (gin-gonic#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
1 parent 7ec82ee commit 521d06c

File tree

8 files changed

+158
-31
lines changed

8 files changed

+158
-31
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
3939
- [Custom Validators](#custom-validators)
4040
- [Only Bind Query String](#only-bind-query-string)
4141
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
42+
- [Bind Uri](#bind-uri)
4243
- [Bind HTML checkboxes](#bind-html-checkboxes)
4344
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
4445
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@@ -793,6 +794,40 @@ Test it with:
793794
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
794795
```
795796

797+
### Bind Uri
798+
799+
See the [detail information](https://github.com/gin-gonic/gin/issues/846).
800+
801+
```go
802+
package main
803+
804+
import "github.com/gin-gonic/gin"
805+
806+
type Person struct {
807+
ID string `uri:"id" binding:"required,uuid"`
808+
Name string `uri:"name" binding:"required"`
809+
}
810+
811+
func main() {
812+
route := gin.Default()
813+
route.GET("/:name/:id", func(c *gin.Context) {
814+
var person Person
815+
if err := c.ShouldBindUri(&person); err != nil {
816+
c.JSON(400, gin.H{"msg": err})
817+
return
818+
}
819+
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
820+
})
821+
route.Run(":8088")
822+
}
823+
```
824+
825+
Test it with:
826+
```sh
827+
$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
828+
$ curl -v localhost:8088/thinkerou/not-uuid
829+
```
830+
796831
### Bind HTML checkboxes
797832

798833
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)

binding/binding.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ type BindingBody interface {
3636
BindBody([]byte, interface{}) error
3737
}
3838

39+
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
40+
// but it read the Params.
41+
type BindingUri interface {
42+
Name() string
43+
BindUri(map[string][]string, interface{}) error
44+
}
45+
3946
// StructValidator is the minimal interface which needs to be implemented in
4047
// order for it to be used as the validator engine for ensuring the correctness
4148
// of the request. Gin provides a default implementation for this using
@@ -70,6 +77,7 @@ var (
7077
ProtoBuf = protobufBinding{}
7178
MsgPack = msgpackBinding{}
7279
YAML = yamlBinding{}
80+
Uri = uriBinding{}
7381
)
7482

7583
// Default returns the appropriate Binding instance based on the HTTP method

binding/binding_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,27 @@ func TestExistsFails(t *testing.T) {
662662
assert.Error(t, err)
663663
}
664664

665+
func TestUriBinding(t *testing.T) {
666+
b := Uri
667+
assert.Equal(t, "uri", b.Name())
668+
669+
type Tag struct {
670+
Name string `uri:"name"`
671+
}
672+
var tag Tag
673+
m := make(map[string][]string)
674+
m["name"] = []string{"thinkerou"}
675+
assert.NoError(t, b.BindUri(m, &tag))
676+
assert.Equal(t, "thinkerou", tag.Name)
677+
678+
type NotSupportStruct struct {
679+
Name map[string]interface{} `uri:"name"`
680+
}
681+
var not NotSupportStruct
682+
assert.Error(t, b.BindUri(m, &not))
683+
assert.Equal(t, "", not.Name)
684+
}
685+
665686
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
666687
b := Form
667688
assert.Equal(t, "form", b.Name())
@@ -1232,3 +1253,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
12321253
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
12331254
return
12341255
}
1256+
1257+
func TestCanSet(t *testing.T) {
1258+
type CanSetStruct struct {
1259+
lowerStart string `form:"lower"`
1260+
}
1261+
1262+
var c CanSetStruct
1263+
assert.Nil(t, mapForm(&c, nil))
1264+
}

binding/form_mapping.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ import (
1212
"time"
1313
)
1414

15+
func mapUri(ptr interface{}, m map[string][]string) error {
16+
return mapFormByTag(ptr, m, "uri")
17+
}
18+
1519
func mapForm(ptr interface{}, form map[string][]string) error {
20+
return mapFormByTag(ptr, form, "form")
21+
}
22+
23+
func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
1624
typ := reflect.TypeOf(ptr).Elem()
1725
val := reflect.ValueOf(ptr).Elem()
1826
for i := 0; i < typ.NumField(); i++ {
@@ -23,7 +31,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
2331
}
2432

2533
structFieldKind := structField.Kind()
26-
inputFieldName := typeField.Tag.Get("form")
34+
inputFieldName := typeField.Tag.Get(tag)
2735
inputFieldNameList := strings.Split(inputFieldName, ",")
2836
inputFieldName = inputFieldNameList[0]
2937
var defaultValue string

binding/uri.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2018 Gin Core Team. All rights reserved.
2+
// Use of this source code is governed by a MIT style
3+
// license that can be found in the LICENSE file.
4+
5+
package binding
6+
7+
type uriBinding struct{}
8+
9+
func (uriBinding) Name() string {
10+
return "uri"
11+
}
12+
13+
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
14+
if err := mapUri(obj, m); err != nil {
15+
return err
16+
}
17+
return validate(obj)
18+
}

context.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,15 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
574574
return c.ShouldBindWith(obj, binding.YAML)
575575
}
576576

577+
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
578+
func (c *Context) ShouldBindUri(obj interface{}) error {
579+
m := make(map[string][]string)
580+
for _, v := range c.Params {
581+
m[v.Key] = []string{v.Value}
582+
}
583+
return binding.Uri.BindUri(m, obj)
584+
}
585+
577586
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
578587
// See the binding package.
579588
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
@@ -585,9 +594,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
585594
//
586595
// NOTE: This method reads the body before binding. So you should use
587596
// ShouldBindWith for better performance if you need to call only once.
588-
func (c *Context) ShouldBindBodyWith(
589-
obj interface{}, bb binding.BindingBody,
590-
) (err error) {
597+
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
591598
var body []byte
592599
if cb, ok := c.Get(BodyBytesKey); ok {
593600
if cbb, ok := cb.([]byte); ok {

githubapi_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,27 @@ var githubAPI = []route{
285285
{"DELETE", "/user/keys/:id"},
286286
}
287287

288+
func TestShouldBindUri(t *testing.T) {
289+
DefaultWriter = os.Stdout
290+
router := Default()
291+
292+
type Person struct {
293+
Name string `uri:"name"`
294+
Id string `uri:"id"`
295+
}
296+
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
297+
var person Person
298+
assert.NoError(t, c.ShouldBindUri(&person))
299+
assert.True(t, "" != person.Name)
300+
assert.True(t, "" != person.Id)
301+
c.String(http.StatusOK, "ShouldBindUri test OK")
302+
})
303+
304+
path, _ := exampleFromPath("/rest/:name/:id")
305+
w := performRequest(router, "GET", path)
306+
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
307+
}
308+
288309
func githubConfigRouter(router *Engine) {
289310
for _, route := range githubAPI {
290311
router.Handle(route.method, route.path, func(c *Context) {

tree_test.go

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,19 @@ func TestTreeWildcard(t *testing.T) {
170170

171171
checkRequests(t, tree, testRequests{
172172
{"/", false, "/", nil},
173-
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
174-
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
175-
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
176-
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
177-
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
173+
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
174+
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
175+
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
176+
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
177+
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
178178
{"/search/", false, "/search/", nil},
179-
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
180-
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
181-
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
182-
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
183-
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
184-
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
185-
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
179+
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
180+
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
181+
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
182+
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
183+
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
184+
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
185+
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
186186
})
187187

188188
checkPriorities(t, tree)
@@ -209,18 +209,18 @@ func TestUnescapeParameters(t *testing.T) {
209209
unescape := true
210210
checkRequests(t, tree, testRequests{
211211
{"/", false, "/", nil},
212-
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
213-
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
214-
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
215-
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
216-
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
217-
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
218-
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
219-
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
220-
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
221-
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
222-
{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
223-
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
212+
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
213+
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
214+
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
215+
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
216+
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
217+
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
218+
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
219+
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
220+
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
221+
{"/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"}}},
222+
{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
223+
{"/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"}}},
224224
}, unescape)
225225

226226
checkPriorities(t, tree)
@@ -326,9 +326,9 @@ func TestTreeDupliatePath(t *testing.T) {
326326
checkRequests(t, tree, testRequests{
327327
{"/", false, "/", nil},
328328
{"/doc/", false, "/doc/", nil},
329-
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
330-
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
331-
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
329+
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
330+
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
331+
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
332332
})
333333
}
334334

0 commit comments

Comments
 (0)