mirror of https://github.com/gin-gonic/gin.git
feat(form): Support default values for collections in form binding (#4048)
This commit is contained in:
parent
9d7c0e9e1a
commit
f05f966a08
|
@ -159,6 +159,14 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||
if k, v := head(opt, "="); k == "default" {
|
||||
setOpt.isDefaultExists = true
|
||||
setOpt.defaultValue = v
|
||||
|
||||
// convert semicolon-separated default values to csv-separated values for processing in setByForm
|
||||
if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" || cfTag == "csv" {
|
||||
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,6 +232,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||
case reflect.Slice:
|
||||
if !ok {
|
||||
vs = []string{opt.defaultValue}
|
||||
|
||||
// pre-process the default value for multi if present
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
vs = strings.Split(opt.defaultValue, ",")
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
|
@ -238,6 +252,12 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||
case reflect.Array:
|
||||
if !ok {
|
||||
vs = []string{opt.defaultValue}
|
||||
|
||||
// pre-process the default value for multi if present
|
||||
cfTag := field.Tag.Get("collection_format")
|
||||
if cfTag == "" || cfTag == "multi" {
|
||||
vs = strings.Split(opt.defaultValue, ",")
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
|
|
|
@ -323,6 +323,72 @@ func TestMappingCollectionFormat(t *testing.T) {
|
|||
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
|
||||
}
|
||||
|
||||
func TestMappingCollectionFormatInvalid(t *testing.T) {
|
||||
var s struct {
|
||||
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{
|
||||
"slice_csv": {"1,2"},
|
||||
}, "form")
|
||||
require.Error(t, err)
|
||||
|
||||
var s2 struct {
|
||||
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
|
||||
}
|
||||
err = mappingByPtr(&s2, formSource{
|
||||
"array_csv": {"1,2"},
|
||||
}, "form")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
|
||||
var s struct {
|
||||
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
|
||||
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
|
||||
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
|
||||
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
|
||||
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
|
||||
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
|
||||
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
|
||||
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
|
||||
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
|
||||
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
|
||||
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
|
||||
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
|
||||
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
|
||||
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
|
||||
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
|
||||
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
|
||||
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
|
||||
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
|
||||
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
|
||||
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
|
||||
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
|
||||
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
|
||||
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
|
||||
assert.Equal(t, []int{1, 2, 3}, 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.ArrayTsv)
|
||||
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
|
||||
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
|
||||
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
|
||||
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
|
||||
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
|
||||
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
|
||||
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
|
||||
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
|
||||
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
|
||||
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
|
||||
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
|
||||
}
|
||||
|
||||
func TestMappingStructField(t *testing.T) {
|
||||
var s struct {
|
||||
J struct {
|
||||
|
|
48
docs/doc.md
48
docs/doc.md
|
@ -26,6 +26,7 @@
|
|||
- [Custom Validators](#custom-validators)
|
||||
- [Only Bind Query String](#only-bind-query-string)
|
||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||
- [Bind default value if none provided](#bind-default-value-if-none-provided)
|
||||
- [Collection format for arrays](#collection-format-for-arrays)
|
||||
- [Bind Uri](#bind-uri)
|
||||
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
|
||||
|
@ -862,6 +863,53 @@ Test it with:
|
|||
curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
|
||||
```
|
||||
|
||||
|
||||
### Bind default value if none provided
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
Name string `form:"name,default=William"`
|
||||
Age int `form:"age,default=10"`
|
||||
Friends []string `form:"friends,default=Will;Bill"`
|
||||
Addresses [2]string `form:"addresses,default=foo bar" collection_format:"ssv"`
|
||||
LapTimes []int `form:"lap_times,default=1;2;3" collection_format:"csv"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
g := gin.Default()
|
||||
g.POST("/person", func(c *gin.Context) {
|
||||
var req Person
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, req)
|
||||
})
|
||||
_ = g.Run("localhost:8080")
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
curl -X POST http://localhost:8080/person
|
||||
{"Name":"William","Age":10,"Friends":["Will","Bill"],"Colors":["red","blue"],"LapTimes":[1,2,3]}
|
||||
```
|
||||
|
||||
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
||||
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
|
||||
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
||||
|
||||
|
||||
#### Collection format for arrays
|
||||
|
||||
| Format | Description | Example |
|
||||
|
|
Loading…
Reference in New Issue