Skip to content

Commit f05f966

Browse files
authored
feat(form): Support default values for collections in form binding (#4048)
1 parent 9d7c0e9 commit f05f966

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

binding/form_mapping.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
159159
if k, v := head(opt, "="); k == "default" {
160160
setOpt.isDefaultExists = true
161161
setOpt.defaultValue = v
162+
163+
// convert semicolon-separated default values to csv-separated values for processing in setByForm
164+
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
165+
cfTag := field.Tag.Get("collection_format")
166+
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
167+
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
168+
}
169+
}
162170
}
163171
}
164172

@@ -224,6 +232,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
224232
case reflect.Slice:
225233
if !ok {
226234
vs = []string{opt.defaultValue}
235+
236+
// pre-process the default value for multi if present
237+
cfTag := field.Tag.Get("collection_format")
238+
if cfTag == "" || cfTag == "multi" {
239+
vs = strings.Split(opt.defaultValue, ",")
240+
}
227241
}
228242

229243
if ok, err = trySetCustom(vs[0], value); ok {
@@ -238,6 +252,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
238252
case reflect.Array:
239253
if !ok {
240254
vs = []string{opt.defaultValue}
255+
256+
// pre-process the default value for multi if present
257+
cfTag := field.Tag.Get("collection_format")
258+
if cfTag == "" || cfTag == "multi" {
259+
vs = strings.Split(opt.defaultValue, ",")
260+
}
241261
}
242262

243263
if ok, err = trySetCustom(vs[0], value); ok {

binding/form_mapping_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,72 @@ func TestMappingCollectionFormat(t *testing.T) {
323323
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
324324
}
325325

326+
func TestMappingCollectionFormatInvalid(t *testing.T) {
327+
var s struct {
328+
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
329+
}
330+
err := mappingByPtr(&s, formSource{
331+
"slice_csv": {"1,2"},
332+
}, "form")
333+
require.Error(t, err)
334+
335+
var s2 struct {
336+
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
337+
}
338+
err = mappingByPtr(&s2, formSource{
339+
"array_csv": {"1,2"},
340+
}, "form")
341+
require.Error(t, err)
342+
}
343+
344+
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
345+
var s struct {
346+
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
347+
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
348+
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
349+
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
350+
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
351+
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
352+
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
353+
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
354+
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
355+
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
356+
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
357+
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
358+
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
359+
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
360+
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
361+
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
362+
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
363+
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
364+
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
365+
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
366+
}
367+
err := mappingByPtr(&s, formSource{}, "form")
368+
require.NoError(t, err)
369+
370+
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
371+
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
372+
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
373+
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
374+
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
375+
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
376+
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
377+
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
378+
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
379+
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
380+
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
381+
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
382+
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
383+
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
384+
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
385+
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
386+
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
387+
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
388+
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
389+
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
390+
}
391+
326392
func TestMappingStructField(t *testing.T) {
327393
var s struct {
328394
J struct {

docs/doc.md

Lines changed: 48 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+
- [Bind default value if none provided](#bind-default-value-if-none-provided)
2930
- [Collection format for arrays](#collection-format-for-arrays)
3031
- [Bind Uri](#bind-uri)
3132
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
@@ -862,6 +863,53 @@ Test it with:
862863
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
863864
```
864865

866+
867+
### Bind default value if none provided
868+
869+
If the server should bind a default value to a field when the client does not provide one, specify the default value using the `default` key within the `form` tag:
870+
871+
```
872+
package main
873+
874+
import (
875+
"net/http"
876+
877+
"github.com/gin-gonic/gin"
878+
)
879+
880+
type Person struct {
881+
Name string `form:"name,default=William"`
882+
Age int `form:"age,default=10"`
883+
Friends []string `form:"friends,default=Will;Bill"`
884+
Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
885+
LapTimes []int `form:"lap_times,default=1;2;3" collection_format:"csv"`
886+
}
887+
888+
func main() {
889+
g := gin.Default()
890+
g.POST("/person", func(c *gin.Context) {
891+
var req Person
892+
if err := c.ShouldBindQuery(&req); err != nil {
893+
c.JSON(http.StatusBadRequest, err)
894+
return
895+
}
896+
c.JSON(http.StatusOK, req)
897+
})
898+
_ = g.Run("localhost:8080")
899+
}
900+
```
901+
902+
```
903+
curl -X POST http://localhost:8080/person
904+
{"Name":"William","Age":10,"Friends":["Will","Bill"],"Colors":["red","blue"],"LapTimes":[1,2,3]}
905+
```
906+
907+
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
908+
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
909+
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
910+
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
911+
912+
865913
#### Collection format for arrays
866914

867915
| Format | Description | Example |

0 commit comments

Comments
 (0)