diff --git a/internal/empty/empty.go b/internal/empty/empty.go index 1d42441ff..023f759a9 100644 --- a/internal/empty/empty.go +++ b/internal/empty/empty.go @@ -85,7 +85,9 @@ func IsEmpty(value interface{}) bool { case map[string]interface{}: return len(value) == 0 default: + // ========================= // Common interfaces checks. + // ========================= if f, ok := value.(apiTime); ok { if f == nil { return true @@ -163,6 +165,126 @@ func IsEmpty(value interface{}) bool { return false } +// IsEmptyLength checks whether given `value` is empty length. +// It returns true if `value` is in: nil, "", len(slice/map/chan) == 0, +// or else it returns false. +//func IsEmptyLength(value interface{}) bool { +// if value == nil { +// return true +// } +// // It firstly checks the variable as common types using assertion to enhance the performance, +// // and then using reflection. +// switch value := value.(type) { +// case +// int, +// int8, +// int16, +// int32, +// int64, +// uint, +// uint8, +// uint16, +// uint32, +// uint64, +// float32, +// float64, +// bool: +// return false +// case string: +// return value == "" +// case []byte: +// return len(value) == 0 +// case []rune: +// return len(value) == 0 +// case []int: +// return len(value) == 0 +// case []string: +// return len(value) == 0 +// case []float32: +// return len(value) == 0 +// case []float64: +// return len(value) == 0 +// case map[string]interface{}: +// return len(value) == 0 +// default: +// // ========================= +// // Common interfaces checks. +// // ========================= +// if f, ok := value.(apiTime); ok { +// if f == nil { +// return true +// } +// return f.IsZero() +// } +// if f, ok := value.(apiString); ok { +// if f == nil { +// return true +// } +// return f.String() == "" +// } +// if f, ok := value.(apiInterfaces); ok { +// if f == nil { +// return true +// } +// return len(f.Interfaces()) == 0 +// } +// if f, ok := value.(apiMapStrAny); ok { +// if f == nil { +// return true +// } +// return len(f.MapStrAny()) == 0 +// } +// // Finally using reflect. +// var rv reflect.Value +// if v, ok := value.(reflect.Value); ok { +// rv = v +// } else { +// rv = reflect.ValueOf(value) +// } +// +// switch rv.Kind() { +// case +// reflect.Int, +// reflect.Int8, +// reflect.Int16, +// reflect.Int32, +// reflect.Int64, +// reflect.Uint, +// reflect.Uint8, +// reflect.Uint16, +// reflect.Uint32, +// reflect.Uint64, +// reflect.Uintptr, +// reflect.Float32, +// reflect.Float64, +// reflect.Bool: +// return false +// case reflect.String: +// return rv.Len() == 0 +// case reflect.Struct: +// for i := 0; i < rv.NumField(); i++ { +// if !IsEmpty(rv) { +// return false +// } +// } +// return true +// case reflect.Chan, +// reflect.Map, +// reflect.Slice, +// reflect.Array: +// return rv.Len() == 0 +// case reflect.Func, +// reflect.Ptr, +// reflect.Interface, +// reflect.UnsafePointer: +// if rv.IsNil() { +// return true +// } +// } +// } +// return false +//} + // IsNil checks whether given `value` is nil. // Parameter `traceSource` is used for tracing to the source variable if given `value` is type // of a pinter that also points to a pointer. It returns nil if the source is nil when `traceSource` 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.go b/util/gvalid/gvalid.go index 42351d218..0b9e18d7e 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -193,6 +193,16 @@ func CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) * return defaultValidator.CheckStruct(object, rules, messages...) } +// CheckStructWithParamMap validates struct with given parameter map and returns the error result. +// +// The parameter `object` should be type of struct/*struct. +// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result +// if `rules` is type of []string. +// The optional parameter `messages` specifies the custom error messages for specified keys and rules. +func CheckStructWithParamMap(object interface{}, paramMap interface{}, rules interface{}, messages ...CustomMsg) *Error { + return defaultValidator.CheckStructWithParamMap(object, paramMap, rules, messages...) +} + // parseSequenceTag parses one sequence tag to field, rule and error message. // The sequence tag is like: [alias@]rule[...#msg...] func parseSequenceTag(tag string) (field, rule, msg string) { diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 49c8505aa..5709092f1 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -6,9 +6,12 @@ package gvalid +import "context" + // Validator is the validation manager. type Validator struct { - i18nLang string // I18n language. + i18nLang string // I18n language. + ctx context.Context // Context containing custom context variables. } // New creates and returns a new Validator. @@ -29,3 +32,10 @@ func (v *Validator) I18n(language string) *Validator { newValidator.i18nLang = language return newValidator } + +// Ctx is a chaining operation function which sets the context for next validation. +func (v *Validator) Ctx(ctx context.Context) *Validator { + newValidator := v.Clone() + newValidator.ctx = ctx + return newValidator +} diff --git a/util/gvalid/gvalid_validator_check.go b/util/gvalid/gvalid_validator_check.go index 831930df1..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" @@ -34,12 +35,12 @@ type apiTime interface { // string/map/struct/*struct. // The optional parameter `params` specifies the extra validation parameters for some rules // like: required-*、same、different, etc. -func (v *Validator) Check(value interface{}, rules string, messages interface{}, params ...interface{}) *Error { - return v.doCheck("", value, rules, messages, params...) +func (v *Validator) Check(value interface{}, rules string, messages interface{}, paramMap ...interface{}) *Error { + return v.doCheck("", value, rules, messages, paramMap...) } // doCheck does the really rules validation for single key-value. -func (v *Validator) doCheck(key string, value interface{}, rules string, messages interface{}, params ...interface{}) *Error { +func (v *Validator) doCheck(key string, value interface{}, rules string, messages interface{}, paramMap ...interface{}) *Error { // If there's no validation rules, it does nothing and returns quickly. if rules == "" { return nil @@ -50,8 +51,8 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message data = make(map[string]interface{}) errorMsgArray = make(map[string]string) ) - if len(params) > 0 { - data = gconv.Map(params[0]) + if len(paramMap) > 0 { + data = gconv.Map(paramMap[0]) } // Custom error messages handling. var ( @@ -107,8 +108,8 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message dataMap map[string]interface{} message = v.getErrorMessageByRule(ruleKey, customMsgMap) ) - if len(params) > 0 { - dataMap = gconv.Map(params[0]) + if len(paramMap) > 0 { + dataMap = gconv.Map(paramMap[0]) } if err := f(ruleItems[index], value, message, dataMap); err != nil { match = false @@ -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 a3005b74c..6aab61a49 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -9,10 +9,20 @@ 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 `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 ( structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. @@ -24,18 +34,52 @@ var ( // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. -func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *Error { +func (v *Validator) CheckStruct(object interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error { + var message CustomMsg + if len(customErrorMessageMap) > 0 { + message = customErrorMessageMap[0] + } + return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{ + Object: object, + ParamMap: nil, + UseParamMapInsteadOfObjectValue: false, + CustomRules: customRules, + CustomErrorMessageMap: message, + }) +} + +// CheckStructWithParamMap validates struct with given parameter map and returns the error result. +// +// The parameter `object` should be type of struct/*struct. +// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result +// if `rules` is type of []string. +// The optional parameter `messages` specifies the custom error messages for specified keys and rules. +func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error { + var message CustomMsg + if len(customErrorMessageMap) > 0 { + message = customErrorMessageMap[0] + } + return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{ + Object: object, + ParamMap: paramMap, + UseParamMapInsteadOfObjectValue: true, + CustomRules: customRules, + CustomErrorMessageMap: message, + }) +} + +func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapInput) *Error { var ( - errorMaps = make(ErrorMap) // Returned error. + errorMaps = make(ErrorMap) // Returning error. ) - mapField, err := structs.FieldMap(object, aliasNameTagPriority, true) + fieldMap, err := structs.FieldMap(input.Object, aliasNameTagPriority, true) if err != nil { return newErrorStr("invalid_object", err.Error()) } // It checks the struct recursively the its attribute is also a struct. - for _, field := range mapField { + for _, field := range fieldMap { if field.OriginalKind() == reflect.Struct { - if err := v.CheckStruct(field.Value, rules, messages...); err != nil { + if err := v.CheckStruct(field.Value, input.CustomRules, input.CustomErrorMessageMap); err != nil { // It merges the errors into single error map. for k, m := range err.errors { errorMaps[k] = m @@ -44,22 +88,23 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages } } // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. - tagField, err := structs.TagFields(object, structTagPriority) + tagField, err := structs.TagFields(input.Object, structTagPriority) if err != nil { return newErrorStr("invalid_object", err.Error()) } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagField) == 0 && rules == nil { + if len(tagField) == 0 && input.CustomRules == nil { return nil } + var ( - params = make(map[string]interface{}) + inputParamMap map[string]interface{} checkRules = make(map[string]string) customMessage = make(CustomMsg) fieldAliases = make(map[string]string) // Alias names for `messages` overwriting struct tag names. errorRules = make([]string, 0) // Sequence rules. ) - switch v := rules.(type) { + switch v := input.CustomRules.(type) { // Sequence tag: []sequence tag // Sequence has order for error results. case []string: @@ -102,11 +147,22 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages if len(tagField) == 0 && len(checkRules) == 0 { return nil } - // Checks and extends the parameters map with struct alias tag. - for nameOrTag, field := range mapField { - params[nameOrTag] = field.Value.Interface() - params[field.Name()] = field.Value.Interface() + // Input parameter map handling. + 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.UseParamMapInsteadOfObjectValue { + for nameOrTag, field := range fieldMap { + inputParamMap[nameOrTag] = field.Value.Interface() + if nameOrTag != field.Name() { + inputParamMap[field.Name()] = field.Value.Interface() + } + } + } + for _, field := range tagField { fieldName := field.Name() // sequence tag == struct tag @@ -118,8 +174,10 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages fieldAliases[fieldName] = name } // It here extends the params map using alias names. - if _, ok := params[name]; !ok { - params[name] = field.Value.Interface() + if _, ok := inputParamMap[name]; !ok { + if !input.UseParamMapInsteadOfObjectValue { + inputParamMap[name] = field.Value.Interface() + } } if _, ok := checkRules[name]; !ok { if _, ok := checkRules[fieldName]; ok { @@ -132,7 +190,7 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages } 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 { @@ -160,8 +218,8 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages // Custom error messages, // which have the most priority than `rules` and struct tag. - if len(messages) > 0 && len(messages[0]) > 0 { - for k, v := range messages[0] { + if len(input.CustomErrorMessageMap) > 0 { + for k, v := range input.CustomErrorMessageMap { if a, ok := fieldAliases[k]; ok { // Overwrite the key of field name. customMessage[a] = v @@ -174,12 +232,9 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages // The following logic is the same as some of CheckMap. var value interface{} for key, rule := range checkRules { - value = nil - if v, ok := params[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], params); e != nil { + if e := v.doCheck(key, value, rule, customMessage[key], inputParamMap); e != nil { _, item := e.FirstItem() // =================================================================== // Only in map and struct validations, if value is nil or empty string 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 188ea4b5c..786145ffb 100755 --- a/util/gvalid/gvalid_z_unit_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_checkstruct_test.go @@ -8,6 +8,7 @@ package gvalid_test import ( "github.com/gogf/gf/container/gvar" + "github.com/gogf/gf/os/gtime" "testing" "github.com/gogf/gf/frame/g" @@ -352,3 +353,62 @@ func Test_CheckStruct_InvalidRule(t *testing.T) { t.AssertNE(err, nil) }) } + +func TestValidator_CheckStructWithParamMap(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + 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.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid" v:"required"` + Nickname string `json:"nickname" v:"required-with:Uid"` + } + data := UserApiSearch{ + Uid: 1, + } + t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) + }) + + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid"` + Nickname string `json:"nickname" v:"required-with:Uid"` + StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"` + EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"` + } + data := UserApiSearch{ + StartTime: nil, + EndTime: nil, + } + t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid"` + Nickname string `json:"nickname" v:"required-with:Uid"` + StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"` + EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"` + } + data := UserApiSearch{ + StartTime: gtime.Now(), + EndTime: nil, + } + t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{"start_time": gtime.Now()}, nil), nil) + }) +}