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" {
|
if k, v := head(opt, "="); k == "default" {
|
||||||
setOpt.isDefaultExists = true
|
setOpt.isDefaultExists = true
|
||||||
setOpt.defaultValue = v
|
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:
|
case reflect.Slice:
|
||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
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 {
|
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:
|
case reflect.Array:
|
||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
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 {
|
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)
|
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) {
|
func TestMappingStructField(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
J struct {
|
J struct {
|
||||||
|
|
48
docs/doc.md
48
docs/doc.md
|
@ -26,6 +26,7 @@
|
||||||
- [Custom Validators](#custom-validators)
|
- [Custom Validators](#custom-validators)
|
||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
- [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)
|
- [Collection format for arrays](#collection-format-for-arrays)
|
||||||
- [Bind Uri](#bind-uri)
|
- [Bind Uri](#bind-uri)
|
||||||
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
|
- [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"
|
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
|
#### Collection format for arrays
|
||||||
|
|
||||||
| Format | Description | Example |
|
| Format | Description | Example |
|
||||||
|
|
Loading…
Reference in New Issue