Support binding for slice/array obj [Rewrite] (#2302)
Co-authored-by: thinkerou <thinkerou@gmail.com> Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
		| @ -51,7 +51,8 @@ type BindingUri interface { | |||||||
| // https://github.com/go-playground/validator/tree/v8.18.2. | // https://github.com/go-playground/validator/tree/v8.18.2. | ||||||
| type StructValidator interface { | type StructValidator interface { | ||||||
| 	// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. | 	// 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 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. | 	// If the struct is not valid or the validation itself fails, a descriptive error should be returned. | ||||||
| 	// Otherwise nil must be returned. | 	// Otherwise nil must be returned. | ||||||
|  | |||||||
| @ -35,7 +35,7 @@ type QueryTest struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type FooStruct 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 { | type FooBarStruct struct { | ||||||
| @ -181,6 +181,20 @@ func TestBindingJSON(t *testing.T) { | |||||||
| 		`{"foo": "bar"}`, `{"bar": "foo"}`) | 		`{"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) { | func TestBindingJSONUseNumber(t *testing.T) { | ||||||
| 	testBodyBindingUseNumber(t, | 	testBodyBindingUseNumber(t, | ||||||
| 		JSON, "json", | 		JSON, "json", | ||||||
| @ -1181,6 +1195,20 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody | |||||||
| 	assert.Error(t, err) | 	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) { | func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) { | ||||||
| 	obj := make(map[string]string) | 	obj := make(map[string]string) | ||||||
| 	req := requestWithBody("POST", path, body) | 	req := requestWithBody("POST", path, body) | ||||||
|  | |||||||
| @ -5,7 +5,9 @@ | |||||||
| package binding | package binding | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
|  |  | ||||||
| 	"github.com/go-playground/validator/v10" | 	"github.com/go-playground/validator/v10" | ||||||
| @ -16,22 +18,54 @@ type defaultValidator struct { | |||||||
| 	validate *validator.Validate | 	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{} | var _ StructValidator = &defaultValidator{} | ||||||
|  |  | ||||||
| // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. | // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. | ||||||
| func (v *defaultValidator) ValidateStruct(obj interface{}) error { | func (v *defaultValidator) ValidateStruct(obj interface{}) error { | ||||||
| 	value := reflect.ValueOf(obj) | 	if obj == nil { | ||||||
| 	valueType := value.Kind() |  | ||||||
| 	if valueType == reflect.Ptr { |  | ||||||
| 		valueType = value.Elem().Kind() |  | ||||||
| 	} |  | ||||||
| 	if valueType == reflect.Struct { |  | ||||||
| 		v.lazyinit() |  | ||||||
| 		if err := v.validate.Struct(obj); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 		return nil | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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 | // Engine returns the underlying validator engine which powers the default | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								binding/default_validator_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								binding/default_validator_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user