Skip to content

Commit 3cb3067

Browse files
authored
feat(form): add array collection format in form binding (#3986)
* feat(form): add array collection format in form binding * feat(form): add array collection format in form binding * test(form): fix test code for array collection format in form binding
1 parent cc4e114 commit 3cb3067

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

binding/form_mapping.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,38 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
182182
return false, nil
183183
}
184184

185+
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
186+
cfTag := field.Tag.Get("collection_format")
187+
if cfTag == "" || cfTag == "multi" {
188+
return vs, nil
189+
}
190+
191+
var sep string
192+
switch cfTag {
193+
case "csv":
194+
sep = ","
195+
case "ssv":
196+
sep = " "
197+
case "tsv":
198+
sep = "\t"
199+
case "pipes":
200+
sep = "|"
201+
default:
202+
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
203+
}
204+
205+
totalLength := 0
206+
for _, v := range vs {
207+
totalLength += strings.Count(v, sep) + 1
208+
}
209+
newVs = make([]string, 0, totalLength)
210+
for _, v := range vs {
211+
newVs = append(newVs, strings.Split(v, sep)...)
212+
}
213+
214+
return newVs, nil
215+
}
216+
185217
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
186218
vs, ok := form[tagValue]
187219
if !ok && !opt.isDefaultExists {
@@ -198,6 +230,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
198230
return ok, err
199231
}
200232

233+
if vs, err = trySplit(vs, field); err != nil {
234+
return false, err
235+
}
236+
201237
return true, setSlice(vs, value, field)
202238
case reflect.Array:
203239
if !ok {
@@ -208,6 +244,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
208244
return ok, err
209245
}
210246

247+
if vs, err = trySplit(vs, field); err != nil {
248+
return false, err
249+
}
250+
211251
if len(vs) != value.Len() {
212252
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
213253
}

binding/form_mapping_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,45 @@ func TestMappingArray(t *testing.T) {
264264
require.Error(t, err)
265265
}
266266

267+
func TestMappingCollectionFormat(t *testing.T) {
268+
var s struct {
269+
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
270+
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
271+
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
272+
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
273+
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
274+
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
275+
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
276+
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
277+
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
278+
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
279+
}
280+
err := mappingByPtr(&s, formSource{
281+
"slice_multi": {"1", "2"},
282+
"slice_csv": {"1,2"},
283+
"slice_ssv": {"1 2"},
284+
"slice_tsv": {"1 2"},
285+
"slice_pipes": {"1|2"},
286+
"array_multi": {"1", "2"},
287+
"array_csv": {"1,2"},
288+
"array_ssv": {"1 2"},
289+
"array_tsv": {"1 2"},
290+
"array_pipes": {"1|2"},
291+
}, "form")
292+
require.NoError(t, err)
293+
294+
assert.Equal(t, []int{1, 2}, s.SliceMulti)
295+
assert.Equal(t, []int{1, 2}, s.SliceCsv)
296+
assert.Equal(t, []int{1, 2}, s.SliceSsv)
297+
assert.Equal(t, []int{1, 2}, s.SliceTsv)
298+
assert.Equal(t, []int{1, 2}, s.SlicePipes)
299+
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
300+
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
301+
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
302+
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
303+
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
304+
}
305+
267306
func TestMappingStructField(t *testing.T) {
268307
var s struct {
269308
J struct {

docs/doc.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- [Custom Validators](#custom-validators)
2727
- [Only Bind Query String](#only-bind-query-string)
2828
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
29+
- [Collection format for arrays](#collection-format-for-arrays)
2930
- [Bind Uri](#bind-uri)
3031
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
3132
- [Bind Header](#bind-header)
@@ -861,6 +862,59 @@ Test it with:
861862
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
862863
```
863864

865+
#### Collection format for arrays
866+
867+
| Format | Description | Example |
868+
| --------------- | --------------------------------------------------------- | ----------------------- |
869+
| multi (default) | Multiple parameter instances rather than multiple values. | key=foo&key=bar&key=baz |
870+
| csv | Comma-separated values. | foo,bar,baz |
871+
| ssv | Space-separated values. | foo bar baz |
872+
| tsv | Tab-separated values. | "foo\tbar\tbaz" |
873+
| pipes | Pipe-separated values. | foo\|bar\|baz |
874+
875+
```go
876+
package main
877+
878+
import (
879+
"log"
880+
"time"
881+
"github.com/gin-gonic/gin"
882+
)
883+
884+
type Person struct {
885+
Name string `form:"name"`
886+
Addresses []string `form:"addresses" collection_format:"csv"`
887+
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
888+
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
889+
UnixTime time.Time `form:"unixTime" time_format:"unix"`
890+
}
891+
892+
func main() {
893+
route := gin.Default()
894+
route.GET("/testing", startPage)
895+
route.Run(":8085")
896+
}
897+
func startPage(c *gin.Context) {
898+
var person Person
899+
// If `GET`, only `Form` binding engine (`query`) used.
900+
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
901+
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
902+
if c.ShouldBind(&person) == nil {
903+
log.Println(person.Name)
904+
log.Println(person.Addresses)
905+
log.Println(person.Birthday)
906+
log.Println(person.CreateTime)
907+
log.Println(person.UnixTime)
908+
}
909+
c.String(200, "Success")
910+
}
911+
```
912+
913+
Test it with:
914+
```sh
915+
$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
916+
```
917+
864918
### Bind Uri
865919

866920
See the [detail information](https://github.com/gin-gonic/gin/issues/846).

0 commit comments

Comments
 (0)