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
This commit is contained in:
		@ -182,6 +182,38 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
 | 
				
			|||||||
	return false, nil
 | 
						return false, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
 | 
				
			||||||
 | 
						cfTag := field.Tag.Get("collection_format")
 | 
				
			||||||
 | 
						if cfTag == "" || cfTag == "multi" {
 | 
				
			||||||
 | 
							return vs, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var sep string
 | 
				
			||||||
 | 
						switch cfTag {
 | 
				
			||||||
 | 
						case "csv":
 | 
				
			||||||
 | 
							sep = ","
 | 
				
			||||||
 | 
						case "ssv":
 | 
				
			||||||
 | 
							sep = " "
 | 
				
			||||||
 | 
						case "tsv":
 | 
				
			||||||
 | 
							sep = "\t"
 | 
				
			||||||
 | 
						case "pipes":
 | 
				
			||||||
 | 
							sep = "|"
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						totalLength := 0
 | 
				
			||||||
 | 
						for _, v := range vs {
 | 
				
			||||||
 | 
							totalLength += strings.Count(v, sep) + 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						newVs = make([]string, 0, totalLength)
 | 
				
			||||||
 | 
						for _, v := range vs {
 | 
				
			||||||
 | 
							newVs = append(newVs, strings.Split(v, sep)...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return newVs, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
 | 
					func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
 | 
				
			||||||
	vs, ok := form[tagValue]
 | 
						vs, ok := form[tagValue]
 | 
				
			||||||
	if !ok && !opt.isDefaultExists {
 | 
						if !ok && !opt.isDefaultExists {
 | 
				
			||||||
@ -198,6 +230,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
 | 
				
			|||||||
			return ok, err
 | 
								return ok, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if vs, err = trySplit(vs, field); err != nil {
 | 
				
			||||||
 | 
								return false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return true, setSlice(vs, value, field)
 | 
							return true, setSlice(vs, value, field)
 | 
				
			||||||
	case reflect.Array:
 | 
						case reflect.Array:
 | 
				
			||||||
		if !ok {
 | 
							if !ok {
 | 
				
			||||||
@ -208,6 +244,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
 | 
				
			|||||||
			return ok, err
 | 
								return ok, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if vs, err = trySplit(vs, field); err != nil {
 | 
				
			||||||
 | 
								return false, err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if len(vs) != value.Len() {
 | 
							if len(vs) != value.Len() {
 | 
				
			||||||
			return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
 | 
								return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -264,6 +264,45 @@ func TestMappingArray(t *testing.T) {
 | 
				
			|||||||
	require.Error(t, err)
 | 
						require.Error(t, err)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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"`
 | 
				
			||||||
 | 
							SliceTsv   []int  `form:"slice_tsv" collection_format:"tsv"`
 | 
				
			||||||
 | 
							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"`
 | 
				
			||||||
 | 
							ArrayTsv   [2]int `form:"array_tsv" collection_format:"tsv"`
 | 
				
			||||||
 | 
							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_tsv":   {"1	2"},
 | 
				
			||||||
 | 
							"slice_pipes": {"1|2"},
 | 
				
			||||||
 | 
							"array_multi": {"1", "2"},
 | 
				
			||||||
 | 
							"array_csv":   {"1,2"},
 | 
				
			||||||
 | 
							"array_ssv":   {"1 2"},
 | 
				
			||||||
 | 
							"array_tsv":   {"1	2"},
 | 
				
			||||||
 | 
							"array_pipes": {"1|2"},
 | 
				
			||||||
 | 
						}, "form")
 | 
				
			||||||
 | 
						require.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.SliceTsv)
 | 
				
			||||||
 | 
						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.ArrayTsv)
 | 
				
			||||||
 | 
						assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestMappingStructField(t *testing.T) {
 | 
					func TestMappingStructField(t *testing.T) {
 | 
				
			||||||
	var s struct {
 | 
						var s struct {
 | 
				
			||||||
		J struct {
 | 
							J struct {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								docs/doc.md
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								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)
 | 
				
			||||||
 | 
					  - [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)
 | 
				
			||||||
  - [Bind Header](#bind-header)
 | 
					  - [Bind Header](#bind-header)
 | 
				
			||||||
@ -861,6 +862,59 @@ 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"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 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             |
 | 
				
			||||||
 | 
					| tsv             | Tab-separated values.                                     | "foo\tbar\tbaz"         |
 | 
				
			||||||
 | 
					| pipes           | Pipe-separated values.                                    | foo\|bar\|baz           |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```go
 | 
				
			||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Person struct {
 | 
				
			||||||
 | 
						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() {
 | 
				
			||||||
 | 
						route := gin.Default()
 | 
				
			||||||
 | 
						route.GET("/testing", startPage)
 | 
				
			||||||
 | 
						route.Run(":8085")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func startPage(c *gin.Context) {
 | 
				
			||||||
 | 
						var person Person
 | 
				
			||||||
 | 
						// If `GET`, only `Form` binding engine (`query`) used.
 | 
				
			||||||
 | 
						// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
 | 
				
			||||||
 | 
						// 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.Addresses)
 | 
				
			||||||
 | 
					                log.Println(person.Birthday)
 | 
				
			||||||
 | 
					                log.Println(person.CreateTime)
 | 
				
			||||||
 | 
					                log.Println(person.UnixTime)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
						c.String(200, "Success")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Test it with:
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					$ curl -X GET "localhost:8085/testing?name=appleboy&addresses=foo,bar&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Bind Uri
 | 
					### Bind Uri
 | 
				
			||||||
 | 
					
 | 
				
			||||||
See the [detail information](https://github.com/gin-gonic/gin/issues/846).
 | 
					See the [detail information](https://github.com/gin-gonic/gin/issues/846).
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user