diff --git a/net/ghttp/ghttp_request_param.go b/net/ghttp/ghttp_request_param.go index f77eeeec3..991967df1 100644 --- a/net/ghttp/ghttp_request_param.go +++ b/net/ghttp/ghttp_request_param.go @@ -82,24 +82,27 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { // 1. {"id":1, "name":"john"} // 2. ?id=1&name=john case reflect.Ptr, reflect.Struct: + var ( + err error + data map[string]interface{} + ) // Converting. switch requestType { case parseTypeQuery: - if err := r.GetQueryStruct(pointer); err != nil { + if data, err = r.doGetQueryStruct(pointer); err != nil { return err } case parseTypeForm: - if err := r.GetFormStruct(pointer); err != nil { + if data, err = r.doGetFormStruct(pointer); err != nil { return err } default: - if err := r.GetStruct(pointer); err != nil { + if data, err = r.doGetRequestStruct(pointer); err != nil { return err } } - // Validation. - if err := gvalid.CheckStruct(pointer, nil); err != nil { + if err := gvalid.CheckStructWithParamMap(pointer, data, nil); err != nil { return err } @@ -107,7 +110,7 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { // [{"id":1, "name":"john"}, {"id":, "name":"smith"}] case reflect.Array, reflect.Slice: // If struct slice conversion, it might post JSON/XML content, - // so it uses gjson for the conversion. + // so it uses `gjson` for the conversion. j, err := gjson.LoadContent(r.GetBody()) if err != nil { return err @@ -116,7 +119,11 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { return err } for i := 0; i < reflectVal2.Len(); i++ { - if err := gvalid.CheckStruct(reflectVal2.Index(i), nil); err != nil { + if err := gvalid.CheckStructWithParamMap( + reflectVal2.Index(i), + j.GetMap(gconv.String(i)), + nil, + ); err != nil { return err } } diff --git a/net/ghttp/ghttp_request_param_form.go b/net/ghttp/ghttp_request_param_form.go index 630e9ea74..2e09b8b00 100644 --- a/net/ghttp/ghttp_request_param_form.go +++ b/net/ghttp/ghttp_request_param_form.go @@ -188,13 +188,18 @@ func (r *Request) GetFormMapStrVar(kvMap ...map[string]interface{}) map[string]* // given struct object. Note that the parameter is a pointer to the struct object. // The optional parameter is used to specify the key to attribute mapping. func (r *Request) GetFormStruct(pointer interface{}, mapping ...map[string]string) error { + _, err := r.doGetFormStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetFormStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { r.parseForm() - data := r.formMap + data = r.formMap if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } diff --git a/net/ghttp/ghttp_request_param_query.go b/net/ghttp/ghttp_request_param_query.go index a7257ecbf..b108b4b3c 100644 --- a/net/ghttp/ghttp_request_param_query.go +++ b/net/ghttp/ghttp_request_param_query.go @@ -196,13 +196,18 @@ func (r *Request) GetQueryMapStrVar(kvMap ...map[string]interface{}) map[string] // to the struct object. The optional parameter is used to specify the key to // attribute mapping. func (r *Request) GetQueryStruct(pointer interface{}, mapping ...map[string]string) error { + _, err := r.doGetQueryStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetQueryStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { r.parseQuery() - data := r.GetQueryMap() + data = r.GetQueryMap() if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } diff --git a/net/ghttp/ghttp_request_param_request.go b/net/ghttp/ghttp_request_param_request.go index 95a2be8e0..2e62a1992 100644 --- a/net/ghttp/ghttp_request_param_request.go +++ b/net/ghttp/ghttp_request_param_request.go @@ -270,14 +270,19 @@ func (r *Request) GetRequestMapStrVar(kvMap ...map[string]interface{}) map[strin // the parameter is a pointer to the struct object. // The optional parameter is used to specify the key to attribute mapping. func (r *Request) GetRequestStruct(pointer interface{}, mapping ...map[string]string) error { - data := r.GetRequestMap() + _, err := r.doGetRequestStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetRequestStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { + data = r.GetRequestMap() if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } // mergeDefaultStructValue merges the request parameters with default values from struct tag definition. diff --git a/net/ghttp/ghttp_unit_request_json_test.go b/net/ghttp/ghttp_unit_request_json_test.go index 656ad6073..09e68c72b 100644 --- a/net/ghttp/ghttp_unit_request_json_test.go +++ b/net/ghttp/ghttp_unit_request_json_test.go @@ -23,7 +23,7 @@ func Test_Params_Json_Request(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` + Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` } p, _ := ports.PopRand() s := g.Server(p) diff --git a/net/ghttp/ghttp_unit_request_struct_test.go b/net/ghttp/ghttp_unit_request_struct_test.go index 4bdd503a8..687c936c4 100644 --- a/net/ghttp/ghttp_unit_request_struct_test.go +++ b/net/ghttp/ghttp_unit_request_struct_test.go @@ -395,7 +395,7 @@ func Test_Params_Struct(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"` + Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"` } p, _ := ports.PopRand() s := g.Server(p) @@ -452,8 +452,8 @@ func Test_Params_Struct(t *testing.T) { t.Assert(client.PostContent("/struct1", `id=1&name=john&password1=123&password2=456`), `1john123456`) t.Assert(client.PostContent("/struct2", `id=1&name=john&password1=123&password2=456`), `1john123456`) t.Assert(client.PostContent("/struct2", ``), ``) - t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`) - t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`) + t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`) + t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`) t.Assert(client.PostContent("/parse", `{"id":1,"name":"john","password1":"123Abc!@#","password2":"123Abc!@#"}`), `1john123Abc!@#123Abc!@#`) }) } @@ -464,7 +464,7 @@ func Test_Params_Structs(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"` + Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"` } p, _ := ports.PopRand() s := g.Server(p) @@ -491,3 +491,39 @@ func Test_Params_Structs(t *testing.T) { ) }) } + +func Test_Params_Struct_Validation(t *testing.T) { + type User struct { + Id int `v:"required"` + Name string `v:"name@required-with:id"` + } + p, _ := ports.PopRand() + s := g.Server(p) + s.Group("/", func(group *ghttp.RouterGroup) { + group.ALL("/", func(r *ghttp.Request) { + var ( + err error + user *User + ) + err = r.Parse(&user) + if err != nil { + r.Response.WriteExit(err) + } + r.Response.WriteExit(user.Id, user.Name) + }) + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + c := g.Client() + c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + t.Assert(c.GetContent("/", ``), `The Id field is required`) + t.Assert(c.GetContent("/", `id=1&name=john`), `1john`) + t.Assert(c.PostContent("/", `id=1&name=john&password1=123&password2=456`), `1john`) + t.Assert(c.PostContent("/", `id=1`), `The name field is required`) + }) +} diff --git a/net/ghttp/ghttp_unit_request_xml_test.go b/net/ghttp/ghttp_unit_request_xml_test.go index 37252b9cc..6dd253d11 100644 --- a/net/ghttp/ghttp_unit_request_xml_test.go +++ b/net/ghttp/ghttp_unit_request_xml_test.go @@ -22,7 +22,7 @@ func Test_Params_Xml_Request(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` + Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` } p, _ := ports.PopRand() s := g.Server(p) diff --git a/util/gvalid/gvalid_validator_check.go b/util/gvalid/gvalid_validator_check.go index d3400fd48..c37e4c66f 100644 --- a/util/gvalid/gvalid_validator_check.go +++ b/util/gvalid/gvalid_validator_check.go @@ -14,6 +14,7 @@ import ( "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "strconv" "strings" "time" @@ -232,8 +233,9 @@ func (v *Validator) doCheckBuildInRules( // Values of two fields should be equal as string. case "same": - if v, ok := dataMap[rulePattern]; ok { - if strings.Compare(valueStr, gconv.String(v)) == 0 { + _, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern) + if foundValue != nil { + if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { match = true } } @@ -247,8 +249,9 @@ func (v *Validator) doCheckBuildInRules( // Values of two fields should not be equal as string. case "different": match = true - if v, ok := dataMap[rulePattern]; ok { - if strings.Compare(valueStr, gconv.String(v)) == 0 { + _, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern) + if foundValue != nil { + if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { match = false } } @@ -302,6 +305,7 @@ func (v *Validator) doCheckBuildInRules( // 16x, 19x case "phone": match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^16[\d]{9}$|^17[0,2,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$|^19[\d]{9}$`, valueStr) + // Loose mobile phone number verification(宽松的手机号验证) // As long as the 11 digit numbers beginning with // 13, 14, 15, 16, 17, 18, 19 can pass the verification (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证) diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 3c690ef0a..fb7df4302 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -9,17 +9,18 @@ package gvalid import ( "github.com/gogf/gf/internal/structs" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "reflect" "strings" ) // doCheckStructWithParamMapInput is used for struct validation for internal function. type doCheckStructWithParamMapInput struct { - Object interface{} // Can be type of struct/*struct. - ParamMap interface{} // Validation parameter map. Note that it acts different according attribute `ParamMapStrict`. - ParamMapStrict bool // Strictly using `ParamMap` as its validation source. If false, it ignores `ParamMap` and retrieves values from `Object`. - CustomRules interface{} // Custom validation rules. - CustomErrorMessageMap CustomMsg // Custom error message map for validation rules. + Object interface{} // Can be type of struct/*struct. + ParamMap interface{} // Validation parameter map. Note that it acts different according attribute `UseParamMapInsteadOfObjectValue`. + UseParamMapInsteadOfObjectValue bool // Using `ParamMap` as its validation source instead of values from `Object`. + CustomRules interface{} // Custom validation rules. + CustomErrorMessageMap CustomMsg // Custom error message map for validation rules. } var ( @@ -39,11 +40,11 @@ func (v *Validator) CheckStruct(object interface{}, customRules interface{}, cus message = customErrorMessageMap[0] } return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{ - Object: object, - ParamMap: nil, - ParamMapStrict: false, - CustomRules: customRules, - CustomErrorMessageMap: message, + Object: object, + ParamMap: nil, + UseParamMapInsteadOfObjectValue: false, + CustomRules: customRules, + CustomErrorMessageMap: message, }) } @@ -59,11 +60,11 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa message = customErrorMessageMap[0] } return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{ - Object: object, - ParamMap: paramMap, - ParamMapStrict: true, - CustomRules: customRules, - CustomErrorMessageMap: message, + Object: object, + ParamMap: paramMap, + UseParamMapInsteadOfObjectValue: true, + CustomRules: customRules, + CustomErrorMessageMap: message, }) } @@ -147,13 +148,13 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn return nil } // Input parameter map handling. - if input.ParamMap == nil || !input.ParamMapStrict { + if input.ParamMap == nil || !input.UseParamMapInsteadOfObjectValue { inputParamMap = make(map[string]interface{}) } else { inputParamMap = gconv.Map(input.ParamMap) } // Checks and extends the parameters map with struct alias tag. - if !input.ParamMapStrict { + if !input.UseParamMapInsteadOfObjectValue { for nameOrTag, field := range fieldMap { inputParamMap[nameOrTag] = field.Value.Interface() if nameOrTag != field.Name() { @@ -174,7 +175,9 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn } // It here extends the params map using alias names. if _, ok := inputParamMap[name]; !ok { - inputParamMap[name] = field.Value.Interface() + if !input.UseParamMapInsteadOfObjectValue { + inputParamMap[name] = field.Value.Interface() + } } if _, ok := checkRules[name]; !ok { if _, ok := checkRules[fieldName]; ok { @@ -187,7 +190,7 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn } errorRules = append(errorRules, name+"@"+rule) } else { - // The passed rules can overwrite the rules in struct tag. + // The input rules can overwrite the rules in struct tag. continue } if len(msg) > 0 { @@ -229,10 +232,7 @@ func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapIn // The following logic is the same as some of CheckMap. var value interface{} for key, rule := range checkRules { - value = nil - if v, ok := inputParamMap[key]; ok { - value = v - } + _, value = gutil.MapPossibleItemByKey(inputParamMap, key) // It checks each rule and its value in loop. if e := v.doCheck(key, value, rule, customMessage[key], inputParamMap); e != nil { _, item := e.FirstItem() diff --git a/util/gvalid/gvalid_validator_rule_required.go b/util/gvalid/gvalid_validator_rule_required.go index 969d7bb56..9e30b704d 100644 --- a/util/gvalid/gvalid_validator_rule_required.go +++ b/util/gvalid/gvalid_validator_rule_required.go @@ -9,6 +9,7 @@ package gvalid import ( "github.com/gogf/gf/internal/empty" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "reflect" "strings" ) @@ -26,17 +27,19 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-if: id,1,age,18 case "required-if": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) // It supports multiple field and value pairs. if len(array)%2 == 0 { for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - if v, ok := dataMap[tk]; ok { - if strings.Compare(tv, gconv.String(v)) == 0 { - required = true - break - } + _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) + if strings.Compare(tv, gconv.String(foundValue)) == 0 { + required = true + break } i += 2 } @@ -46,18 +49,21 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-unless: id,1,age,18 case "required-unless": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) // It supports multiple field and value pairs. if len(array)%2 == 0 { for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - if v, ok := dataMap[tk]; ok { - if strings.Compare(tv, gconv.String(v)) == 0 { - required = false - break - } + _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) + if strings.Compare(tv, gconv.String(foundValue)) == 0 { + required = false + break } + i += 2 } } @@ -66,9 +72,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-with": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if !empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if !empty.IsEmpty(foundValue) { required = true break } @@ -78,9 +88,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-with-all": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if empty.IsEmpty(foundValue) { required = false break } @@ -90,9 +104,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-without": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if empty.IsEmpty(foundValue) { required = true break } @@ -102,9 +120,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-without-all": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if !empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if !empty.IsEmpty(foundValue) { required = false break } diff --git a/util/gvalid/gvalid_z_unit_checkstruct_test.go b/util/gvalid/gvalid_z_unit_checkstruct_test.go index 0f2446dcd..786145ffb 100755 --- a/util/gvalid/gvalid_z_unit_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_checkstruct_test.go @@ -357,11 +357,22 @@ func Test_CheckStruct_InvalidRule(t *testing.T) { func TestValidator_CheckStructWithParamMap(t *testing.T) { gtest.C(t, func(t *gtest.T) { type UserApiSearch struct { - Uid int64 `json:"uid" v:"required"` - Nickname string `json:"nickname" v:"required-with:Uid"` + Uid int64 `v:"required"` + Nickname string `v:"required-with:uid"` + } + data := UserApiSearch{ + Uid: 1, + Nickname: "john", + } + t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{"uid": 1, "nickname": "john"}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `v:"required"` + Nickname string `v:"required-with:uid"` } data := UserApiSearch{} - t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) + t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) }) gtest.C(t, func(t *gtest.T) { type UserApiSearch struct { @@ -398,6 +409,6 @@ func TestValidator_CheckStructWithParamMap(t *testing.T) { StartTime: gtime.Now(), EndTime: nil, } - t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) + t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{"start_time": gtime.Now()}, nil), nil) }) }