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).
|
||||
|
||||
#### 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
|
||||
package main
|
||||
|
||||
@ -887,11 +896,11 @@ import (
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string `form:"name"`
|
||||
Address string `form:"address"`
|
||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
Name string `form:"name"`
|
||||
Addresses []string `form:"addresses" collection_format:"csv"`
|
||||
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
|
||||
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
|
||||
UnixTime time.Time `form:"unixTime" time_format:"unix"`
|
||||
}
|
||||
|
||||
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
|
||||
if c.ShouldBind(&person) == nil {
|
||||
log.Println(person.Name)
|
||||
log.Println(person.Address)
|
||||
log.Println(person.Addresses)
|
||||
log.Println(person.Birthday)
|
||||
log.Println(person.CreateTime)
|
||||
log.Println(person.UnixTime)
|
||||
@ -919,7 +928,7 @@ func startPage(c *gin.Context) {
|
||||
|
||||
Test it with:
|
||||
```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
|
||||
|
@ -114,6 +114,13 @@ type FooStructForSliceType struct {
|
||||
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 {
|
||||
StructFoo struct {
|
||||
Idx int `form:"idx"`
|
||||
@ -311,6 +318,15 @@ func TestBindingFormInvalidName2(t *testing.T) {
|
||||
"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) {
|
||||
testFormBindingForType(t, "POST",
|
||||
"/", "/",
|
||||
@ -1065,6 +1081,24 @@ func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badB
|
||||
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) {
|
||||
b := Form
|
||||
assert.Equal(t, "form", b.Name())
|
||||
|
@ -170,11 +170,15 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
case reflect.Slice:
|
||||
if !ok {
|
||||
vs = []string{opt.defaultValue}
|
||||
} else {
|
||||
vs = split(vs, field)
|
||||
}
|
||||
return true, setSlice(vs, value, field)
|
||||
case reflect.Array:
|
||||
if !ok {
|
||||
vs = []string{opt.defaultValue}
|
||||
} else {
|
||||
vs = split(vs, field)
|
||||
}
|
||||
if len(vs) != value.Len() {
|
||||
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):]
|
||||
}
|
||||
|
||||
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 {
|
||||
el := reflect.TypeOf(ptr).Elem()
|
||||
|
||||
|
@ -74,6 +74,39 @@ func TestMappingDefault(t *testing.T) {
|
||||
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) {
|
||||
var s struct {
|
||||
A int
|
||||
|
Loading…
x
Reference in New Issue
Block a user