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. | ||||
| 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. | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -5,7 +5,9 @@ | ||||
| package binding | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/go-playground/validator/v10" | ||||
| @ -16,24 +18,56 @@ 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 valueType == reflect.Struct { | ||||
| 		v.lazyinit() | ||||
| 		if err := v.validate.Struct(obj); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	if obj == 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 | ||||
| // Validator instance. This is useful if you want to register custom validations | ||||
| // or struct level validations. See validator GoDoc for more info - | ||||
|  | ||||
							
								
								
									
										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