mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-17 22:32:26 +08:00
Array collection format in form binding
This commit is contained in:
parent
34ce2104ca
commit
ddcc2ed355
23
README.md
23
README.md
@ -876,6 +876,15 @@ func startPage(c *gin.Context) {
|
|||||||
|
|
||||||
See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
|
See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
|
||||||
|
|
||||||
|
#### Collection format for arrays
|
||||||
|
|
||||||
|
| Format | Description | Example |
|
||||||
|
| --------------- | --------------------------------------------------------- | ----------------------- |
|
||||||
|
| multi (default) | Multiple parameter instances rather than multiple values. | key=foo&key=bar&key=baz |
|
||||||
|
| csv | Comma-separated values. | foo,bar,baz |
|
||||||
|
| ssv | Space-separated values. | foo bar baz |
|
||||||
|
| pipes | Pipe-separated values. | foo\|bar\|baz |
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@ -887,11 +896,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Person struct {
|
type Person struct {
|
||||||
Name string `form:"name"`
|
Name string `form:"name"`
|
||||||
Address string `form:"address"`
|
Addresses []string `form:"addresses" collection_format:"csv"`
|
||||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -907,7 +916,7 @@ func startPage(c *gin.Context) {
|
|||||||
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
|
||||||
if c.ShouldBind(&person) == nil {
|
if c.ShouldBind(&person) == nil {
|
||||||
log.Println(person.Name)
|
log.Println(person.Name)
|
||||||
log.Println(person.Address)
|
log.Println(person.Addresses)
|
||||||
log.Println(person.Birthday)
|
log.Println(person.Birthday)
|
||||||
log.Println(person.CreateTime)
|
log.Println(person.CreateTime)
|
||||||
log.Println(person.UnixTime)
|
log.Println(person.UnixTime)
|
||||||
@ -919,7 +928,7 @@ func startPage(c *gin.Context) {
|
|||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
```sh
|
```sh
|
||||||
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Bind Uri
|
### Bind Uri
|
||||||
|
@ -114,6 +114,13 @@ type FooStructForSliceType struct {
|
|||||||
SliceFoo []int `form:"slice_foo"`
|
SliceFoo []int `form:"slice_foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FooStructForCollectionFormatTag struct {
|
||||||
|
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
|
||||||
|
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
|
||||||
|
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
|
||||||
|
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
|
||||||
|
}
|
||||||
|
|
||||||
type FooStructForStructType struct {
|
type FooStructForStructType struct {
|
||||||
StructFoo struct {
|
StructFoo struct {
|
||||||
Idx int `form:"idx"`
|
Idx int `form:"idx"`
|
||||||
@ -311,6 +318,15 @@ func TestBindingFormInvalidName2(t *testing.T) {
|
|||||||
"map_foo=bar", "bar2=foo")
|
"map_foo=bar", "bar2=foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBindingFormCollectionFormat(t *testing.T) {
|
||||||
|
testFormBindingForCollectionFormat(t, "POST",
|
||||||
|
"/?slice_multi=1&slice_multi=2&slice_csv=1,2&slice_ssv=1 2&slice_pipes=1|2", "/",
|
||||||
|
"", "")
|
||||||
|
testFormBindingForCollectionFormat(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"slice_multi=1&slice_multi=2&slice_csv=1,2&slice_ssv=1 2&slice_pipes=1|2", "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingFormForType(t *testing.T) {
|
func TestBindingFormForType(t *testing.T) {
|
||||||
testFormBindingForType(t, "POST",
|
testFormBindingForType(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -1065,6 +1081,24 @@ func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badB
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFormBindingForCollectionFormat(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
|
b := Form
|
||||||
|
assert.Equal(t, "form", b.Name())
|
||||||
|
|
||||||
|
obj := FooStructForCollectionFormatTag{}
|
||||||
|
req := requestWithBody(method, path, body)
|
||||||
|
if method == "POST" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []int{1, 2}, obj.SliceMulti)
|
||||||
|
assert.Equal(t, []int{1, 2}, obj.SliceCsv)
|
||||||
|
assert.Equal(t, []int{1, 2}, obj.SliceCsv)
|
||||||
|
assert.Equal(t, []int{1, 2}, obj.SlicePipes)
|
||||||
|
}
|
||||||
|
|
||||||
func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) {
|
func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
assert.Equal(t, "form", b.Name())
|
||||||
|
@ -170,11 +170,15 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
case reflect.Slice:
|
case reflect.Slice:
|
||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
vs = []string{opt.defaultValue}
|
||||||
|
} else {
|
||||||
|
vs = split(vs, field)
|
||||||
}
|
}
|
||||||
return true, setSlice(vs, value, field)
|
return true, setSlice(vs, value, field)
|
||||||
case reflect.Array:
|
case reflect.Array:
|
||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
vs = []string{opt.defaultValue}
|
||||||
|
} else {
|
||||||
|
vs = split(vs, field)
|
||||||
}
|
}
|
||||||
if len(vs) != value.Len() {
|
if len(vs) != value.Len() {
|
||||||
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||||
@ -373,6 +377,25 @@ func head(str, sep string) (head string, tail string) {
|
|||||||
return str[:idx], str[idx+len(sep):]
|
return str[:idx], str[idx+len(sep):]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func split(vals []string, field reflect.StructField) []string {
|
||||||
|
if cfTag := field.Tag.Get("collection_format"); cfTag != "" {
|
||||||
|
sep := "multi"
|
||||||
|
switch cfTag {
|
||||||
|
case "csv":
|
||||||
|
sep = ","
|
||||||
|
case "ssv":
|
||||||
|
sep = " "
|
||||||
|
case "pipes":
|
||||||
|
sep = "|"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sep != "multi" && len(vals) == 1 {
|
||||||
|
return strings.Split(vals[0], sep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals
|
||||||
|
}
|
||||||
|
|
||||||
func setFormMap(ptr interface{}, form map[string][]string) error {
|
func setFormMap(ptr interface{}, form map[string][]string) error {
|
||||||
el := reflect.TypeOf(ptr).Elem()
|
el := reflect.TypeOf(ptr).Elem()
|
||||||
|
|
||||||
|
@ -74,6 +74,39 @@ func TestMappingDefault(t *testing.T) {
|
|||||||
assert.Equal(t, [1]int{9}, s.Array)
|
assert.Equal(t, [1]int{9}, s.Array)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMappingCollectionFormat(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
|
||||||
|
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
|
||||||
|
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
|
||||||
|
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
|
||||||
|
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
|
||||||
|
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
|
||||||
|
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
|
||||||
|
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{
|
||||||
|
"slice_multi": {"1", "2"},
|
||||||
|
"slice_csv": {"1,2"},
|
||||||
|
"slice_ssv": {"1 2"},
|
||||||
|
"slice_pipes": {"1|2"},
|
||||||
|
"array_multi": {"1", "2"},
|
||||||
|
"array_csv": {"1,2"},
|
||||||
|
"array_ssv": {"1 2"},
|
||||||
|
"array_pipes": {"1|2"},
|
||||||
|
}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []int{1, 2}, s.SliceMulti)
|
||||||
|
assert.Equal(t, []int{1, 2}, s.SliceCsv)
|
||||||
|
assert.Equal(t, []int{1, 2}, s.SliceSsv)
|
||||||
|
assert.Equal(t, []int{1, 2}, s.SlicePipes)
|
||||||
|
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
|
||||||
|
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
|
||||||
|
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
|
||||||
|
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMappingSkipField(t *testing.T) {
|
func TestMappingSkipField(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
A int
|
A int
|
||||||
|
Loading…
x
Reference in New Issue
Block a user