Array collection format in form binding

This commit is contained in:
Simon Sääw 2021-06-13 13:45:52 +02:00
parent 34ce2104ca
commit ddcc2ed355
4 changed files with 106 additions and 7 deletions

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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