Repository: gin-gonic/gin
Pull request #2302 (2020-03-26 08:47:33)

Support binding for slice/array obj [Rewrite]
support usage followed:

```golang
type StructExample struct {
    A string `json:"a" form:"a" binding"required,max=16"`
}

func exampleHandler(c *gin.Context) {
    var data []StructExample

    if c.Bind(&data) != nil {
        return
    }
    c.JSON(http.StatusOK, data)
}
```

**this pull is rewritten for https://github.com/gin-gonic/gin/pull/2194， avoid to change the validator interface**




wuhuizuo: @thinkerou @appleboy I have rewritten for review issues in !2194
codecov[bot]: # [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=h1) Report
> Merging [#2302](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=desc) into [master](https://codecov.io/gh/gin-gonic/gin/commit/bd5ee1aae2e34e45c7401c3b11c70ac8feac7b41&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/gin-gonic/gin/pull/2302/graphs/tree.svg?width=650&height=150&src=pr&token=Qpqxhwery8)](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2302      +/-   ##
==========================================
+ Coverage   98.47%   98.49%   +0.01%     
==========================================
  Files          41       41              
  Lines        2298     2319      +21     
==========================================
+ Hits         2263     2284      +21     
  Misses         20       20              
  Partials       15       15              
```


| [Impacted Files](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [binding/binding.go](https://codecov.io/gh/gin-gonic/gin/pull/2302/diff?src=pr&el=tree#diff-YmluZGluZy9iaW5kaW5nLmdv) | `100.00% <ø> (ø)` | |
| [binding/default\_validator.go](https://codecov.io/gh/gin-gonic/gin/pull/2302/diff?src=pr&el=tree#diff-YmluZGluZy9kZWZhdWx0X3ZhbGlkYXRvci5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=footer). Last update [bd5ee1a...f5e2565](https://codecov.io/gh/gin-gonic/gin/pull/2302?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

appleboy: @wuhuizuo Please fix the conflicts. 
wuhuizuo: > @wuhuizuo Please fix the conflicts.

done.
appleboy: @wuhuizuo build fail: https://travis-ci.org/github/gin-gonic/gin/builds/740820459
appleboy: LGTM but build fail in go master version. See the report: https://travis-ci.org/github/gin-gonic/gin/jobs/742947069
thinkerou: @appleboy related issue: #2553

Patch:
diff --git a/binding/binding.go b/binding/binding.go
index 5756284..5c8e235 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -51,7 +51,8 @@ type BindingUri interface {
 // https://github.com/go-playground/validator/tree/v8.18.2.
 type StructValidator interface {
 	// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
-	// If the received type is not a struct, any validation should be skipped and nil must be returned.
+	// If the received type is a slice|array, the validation should be performed travel on every element.
+	// If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned.
 	// If the received type is a struct or pointer to a struct, the validation should be performed.
 	// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
 	// Otherwise nil must be returned.
diff --git a/binding/default_validator.go b/binding/default_validator.go
index a4c1a7f..c57a120 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -5,7 +5,9 @@
 package binding
 
 import (
+	"fmt"
 	"reflect"
+	"strings"
 	"sync"
 
 	"github.com/go-playground/validator/v10"
@@ -16,22 +18,54 @@ type defaultValidator struct {
 	validate *validator.Validate
 }
 
+type sliceValidateError []error
+
+func (err sliceValidateError) Error() string {
+	var errMsgs []string
+	for i, e := range err {
+		if e == nil {
+			continue
+		}
+		errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error()))
+	}
+	return strings.Join(errMsgs, "\n")
+}
+
 var _ StructValidator = &defaultValidator{}
 
 // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
 func (v *defaultValidator) ValidateStruct(obj interface{}) error {
-	value := reflect.ValueOf(obj)
-	valueType := value.Kind()
-	if valueType == reflect.Ptr {
-		valueType = value.Elem().Kind()
+	if obj == nil {
+		return nil
 	}
-	if valueType == reflect.Struct {
-		v.lazyinit()
-		if err := v.validate.Struct(obj); err != nil {
-			return err
+
+	value := reflect.ValueOf(obj)
+	switch value.Kind() {
+	case reflect.Ptr:
+		return v.ValidateStruct(value.Elem().Interface())
+	case reflect.Struct:
+		return v.validateStruct(obj)
+	case reflect.Slice, reflect.Array:
+		count := value.Len()
+		validateRet := make(sliceValidateError, 0)
+		for i := 0; i < count; i++ {
+			if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
+				validateRet = append(validateRet, err)
+			}
+		}
+		if len(validateRet) == 0 {
+			return nil
 		}
+		return validateRet
+	default:
+		return nil
 	}
-	return nil
+}
+
+// validateStruct receives struct type
+func (v *defaultValidator) validateStruct(obj interface{}) error {
+	v.lazyinit()
+	return v.validate.Struct(obj)
 }
 
 // Engine returns the underlying validator engine which powers the default


Test patch:
diff --git a/binding/binding_test.go b/binding/binding_test.go
index c354be9..1733617 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -35,7 +35,7 @@ type QueryTest struct {
 }
 
 type FooStruct struct {
-	Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
+	Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"`
 }
 
 type FooBarStruct struct {
@@ -181,6 +181,20 @@ func TestBindingJSON(t *testing.T) {
 		`{"foo": "bar"}`, `{"bar": "foo"}`)
 }
 
+func TestBindingJSONSlice(t *testing.T) {
+	EnableDecoderDisallowUnknownFields = true
+	defer func() {
+		EnableDecoderDisallowUnknownFields = false
+	}()
+
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``)
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`)
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`)
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`)
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`)
+	testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`)
+}
+
 func TestBindingJSONUseNumber(t *testing.T) {
 	testBodyBindingUseNumber(t,
 		JSON, "json",
@@ -1181,6 +1195,20 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
 	assert.Error(t, err)
 }
 
+func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+	assert.Equal(t, name, b.Name())
+
+	var obj1 []FooStruct
+	req := requestWithBody("POST", path, body)
+	err := b.Bind(req, &obj1)
+	assert.NoError(t, err)
+
+	var obj2 []FooStruct
+	req = requestWithBody("POST", badPath, badBody)
+	err = JSON.Bind(req, &obj2)
+	assert.Error(t, err)
+}
+
 func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) {
 	obj := make(map[string]string)
 	req := requestWithBody("POST", path, body)
diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go
new file mode 100644
index 0000000..e9c6de4
--- /dev/null
+++ b/binding/default_validator_test.go
@@ -0,0 +1,68 @@
+// Copyright 2020 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"errors"
+	"testing"
+)
+
+func TestSliceValidateError(t *testing.T) {
+	tests := []struct {
+		name string
+		err  sliceValidateError
+		want string
+	}{
+		{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := tt.err.Error(); got != tt.want {
+				t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestDefaultValidator(t *testing.T) {
+	type exampleStruct struct {
+		A string `binding:"max=8"`
+		B int    `binding:"gt=0"`
+	}
+	tests := []struct {
+		name    string
+		v       *defaultValidator
+		obj     interface{}
+		wantErr bool
+	}{
+		{"validate nil obj", &defaultValidator{}, nil, false},
+		{"validate int obj", &defaultValidator{}, 3, false},
+		{"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true},
+		{"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true},
+		{"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false},
+		{"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true},
+		{"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true},
+		{"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false},
+		{"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true},
+		{"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true},
+		{"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false},
+		{"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true},
+		{"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true},
+		{"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false},
+		{"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true},
+		{"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true},
+		{"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false},
+		{"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true},
+		{"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true},
+		{"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr {
+				t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}