From ddcc2ed3551f25bc38050998edb239346233c20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20S=C3=A4=C3=A4w?= Date: Sun, 13 Jun 2021 13:45:52 +0200 Subject: [PATCH] Array collection format in form binding --- README.md | 23 ++++++++++++++++------- binding/binding_test.go | 34 ++++++++++++++++++++++++++++++++++ binding/form_mapping.go | 23 +++++++++++++++++++++++ binding/form_mapping_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 38c674870f..f1f1042126 100644 --- a/README.md +++ b/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 diff --git a/binding/binding_test.go b/binding/binding_test.go index 1733617724..dfcb809c83 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -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()) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 421c0f7124..defd435aec 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -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() diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 2675d46ba2..3f09d67033 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -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