From fa1814ff5405d7ab772662b5b299c240faf0341f Mon Sep 17 00:00:00 2001 From: John Guo Date: Sat, 29 May 2021 11:30:34 +0800 Subject: [PATCH] add custom rule fucntions feature for package gvalid --- .example/i18n/gi18n/http_view_i18n.go | 2 +- .example/i18n/gi18n/i18n/zh-CN.toml | 4 +- util/gvalid/gvalid_custom_rule.go | 6 +- util/gvalid/gvalid_validator.go | 47 ++++-- util/gvalid/gvalid_validator_check_map.go | 4 +- util/gvalid/gvalid_validator_check_struct.go | 18 ++- ...eck.go => gvalid_validator_check_value.go} | 53 ++++--- util/gvalid/gvalid_z_example_test.go | 33 ++-- util/gvalid/gvalid_z_unit_custom_rule_test.go | 143 ++++++++++++++++-- 9 files changed, 236 insertions(+), 74 deletions(-) rename util/gvalid/{gvalid_validator_check.go => gvalid_validator_check_value.go} (87%) diff --git a/.example/i18n/gi18n/http_view_i18n.go b/.example/i18n/gi18n/http_view_i18n.go index 1f1cc2302..cbf0b6e36 100644 --- a/.example/i18n/gi18n/http_view_i18n.go +++ b/.example/i18n/gi18n/http_view_i18n.go @@ -10,7 +10,7 @@ func main() { s := g.Server() s.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(func(r *ghttp.Request) { - r.SetCtx(gi18n.WithLanguage(r.Context(), "zh-CN")) + r.SetCtx(gi18n.WithLanguage(r.Context(), r.GetString("lang", "zh-CN"))) r.Middleware.Next() }) group.ALL("/", func(r *ghttp.Request) { diff --git a/.example/i18n/gi18n/i18n/zh-CN.toml b/.example/i18n/gi18n/i18n/zh-CN.toml index 80acf06de..20406d93a 100644 --- a/.example/i18n/gi18n/i18n/zh-CN.toml +++ b/.example/i18n/gi18n/i18n/zh-CN.toml @@ -1 +1,3 @@ -OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" \ No newline at end of file +OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" +hello = "你好" +world = "世界" \ No newline at end of file diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 85e431f58..d1e3589bb 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -12,9 +12,9 @@ import "context" // The parameter `rule` specifies the validation rule string, like "required", "between:1,100", etc. // The parameter `value` specifies the value for this rule to validate. // The parameter `message` specifies the custom error message or configured i18n message for this rule. -// The parameter `params` specifies all the parameters that needs. You can ignore parameter `params` if -// you do not really need it in your custom validation rule. -type RuleFunc func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error +// The parameter `data` specifies the `data` which is passed to the Validator. It might be type of map/struct or a nil value. +// You can ignore the parameter `data` if you do not really need it in your custom validation rule. +type RuleFunc func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error var ( // customRuleFuncMap stores the custom rule functions. diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index a1592e362..e46c70295 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -13,21 +13,23 @@ import ( // Validator is the validation manager for chaining operations. type Validator struct { - ctx context.Context // Context containing custom context variables. - i18nManager *gi18n.Manager // I18n manager for error message translation. - key string // Single validation key. - value interface{} // Single validation value. - data interface{} // Validation data, which is usually a map. - rules interface{} // Custom validation data. - messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. - useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`. + ctx context.Context // Context containing custom context variables. + i18nManager *gi18n.Manager // I18n manager for error message translation. + key string // Single validation key. + value interface{} // Single validation value. + data interface{} // Validation data, which is usually a map. + rules interface{} // Custom validation data. + messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. + ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. + useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`. } // New creates and returns a new Validator. func New() *Validator { return &Validator{ - ctx: context.TODO(), // Initialize an empty context. - i18nManager: gi18n.Instance(), // Use default i18n manager. + ctx: context.TODO(), // Initialize an empty context. + i18nManager: gi18n.Instance(), // Use default i18n manager. + ruleFuncMap: make(map[string]RuleFunc), // Custom rule function storing map. } } @@ -77,3 +79,28 @@ func (v *Validator) Messages(messages interface{}) *Validator { newValidator.messages = messages return newValidator } + +// RuleFunc registers one custom rule function to current Validator. +func (v *Validator) RuleFunc(rule string, f RuleFunc) *Validator { + newValidator := v.Clone() + newValidator.ruleFuncMap[rule] = f + return newValidator +} + +// RuleFuncMap registers multiple custom rule functions to current Validator. +func (v *Validator) RuleFuncMap(m map[string]RuleFunc) *Validator { + newValidator := v.Clone() + for k, v := range m { + newValidator.ruleFuncMap[k] = v + } + return newValidator +} + +// getRuleFunc retrieves and returns the custom rule function for specified rule. +func (v *Validator) getRuleFunc(rule string) RuleFunc { + ruleFunc := v.ruleFuncMap[rule] + if ruleFunc == nil { + ruleFunc = customRuleFuncMap[rule] + } + return ruleFunc +} diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index cefb987ca..e7e88d67e 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -96,7 +96,7 @@ func (v *Validator) doCheckMap(params interface{}) Error { value = v } // It checks each rule and its value in loop. - if e := v.doCheckValue(key, value, rule, customMsgs[key], data); e != nil { + if e := v.doCheckValue(key, value, rule, customMsgs[key], params, data); e != nil { _, item := e.FirstItem() // =========================================================== // Only in map and struct validations, if value is nil or empty @@ -112,7 +112,7 @@ func (v *Validator) doCheckMap(params interface{}) Error { break } // Custom rules are also required in default. - if _, ok := customRuleFuncMap[k]; ok { + if f := v.getRuleFunc(k); f != nil { required = true break } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 55e0d7c33..2c921568b 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -57,12 +57,16 @@ func (v *Validator) doCheckStruct(object interface{}) Error { } var ( - 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. + inputParamMap map[string]interface{} + checkRules = make(map[string]string) + customMessage = make(CustomMsg) + checkValueData = v.data + fieldAliases = make(map[string]string) // Alias names for `messages` overwriting struct tag names. + errorRules = make([]string, 0) // Sequence rules. ) + if checkValueData == nil { + checkValueData = object + } switch v := v.rules.(type) { // Sequence tag: []sequence tag // Sequence has order for error results. @@ -194,7 +198,7 @@ func (v *Validator) doCheckStruct(object interface{}) Error { for key, rule := range checkRules { _, value = gutil.MapPossibleItemByKey(inputParamMap, key) // It checks each rule and its value in loop. - if e := v.doCheckValue(key, value, rule, customMessage[key], inputParamMap); e != nil { + if e := v.doCheckValue(key, value, rule, customMessage[key], checkValueData, inputParamMap); e != nil { _, item := e.FirstItem() // =================================================================== // Only in map and struct validations, if value is nil or empty string @@ -210,7 +214,7 @@ func (v *Validator) doCheckStruct(object interface{}) Error { break } // Custom rules are also required in default. - if _, ok := customRuleFuncMap[k]; ok { + if f := v.getRuleFunc(k); f != nil { required = true break } diff --git a/util/gvalid/gvalid_validator_check.go b/util/gvalid/gvalid_validator_check_value.go similarity index 87% rename from util/gvalid/gvalid_validator_check.go rename to util/gvalid/gvalid_validator_check_value.go index 9c01f21c2..1a6493c3e 100644 --- a/util/gvalid/gvalid_validator_check.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -29,11 +29,24 @@ type apiTime interface { // CheckValue checks single value with specified rules. // It returns nil if successful validation. func (v *Validator) CheckValue(value interface{}) Error { - return v.doCheckValue("", value, gconv.String(v.rules), v.messages, v.data) + return v.doCheckValue("", value, gconv.String(v.rules), v.messages, v.data, gconv.Map(v.data)) } // doCheckSingleValue does the really rules validation for single key-value. -func (v *Validator) doCheckValue(key string, value interface{}, rules string, messages interface{}, paramMap ...interface{}) Error { +// +// The parameter `rules` specifies the validation rules string, like "required", "required|between:1,100", etc. +// The parameter `value` specifies the value for this rules to be validated. +// The parameter `messages` specifies the custom error messages for this rule, which is usually type of map/slice. +// The parameter `dataRaw` specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value. +// The parameter `dataMap` specifies the map that is converted from `dataRaw`. It is usually used internally +func (v *Validator) doCheckValue( + key string, + value interface{}, + rules string, + messages interface{}, + dataRaw interface{}, + dataMap map[string]interface{}, +) Error { // If there's no validation rules, it does nothing and returns quickly. if rules == "" { return nil @@ -41,12 +54,8 @@ func (v *Validator) doCheckValue(key string, value interface{}, rules string, me // It converts value to string and then does the validation. var ( // Do not trim it as the space is also part of the value. - data = make(map[string]interface{}) errorMsgArray = make(map[string]string) ) - if len(paramMap) > 0 && paramMap[0] != nil { - data = gconv.Map(paramMap[0]) - } // Custom error messages handling. var ( msgArray = make([]string, 0) @@ -66,7 +75,7 @@ func (v *Validator) doCheckValue(key string, value interface{}, rules string, me for i := 0; ; { array := strings.Split(ruleItems[i], ":") _, ok := allSupportedRules[array[0]] - if !ok && customRuleFuncMap[array[0]] == nil { + if !ok && v.getRuleFunc(array[0]) == nil { if i > 0 && ruleItems[i-1][:5] == "regex" { ruleItems[i-1] += "|" + ruleItems[i] ruleItems = append(ruleItems[:i], ruleItems[i+1:]...) @@ -85,26 +94,26 @@ func (v *Validator) doCheckValue(key string, value interface{}, rules string, me } for index := 0; index < len(ruleItems); { var ( - err error - match = false - results = ruleRegex.FindStringSubmatch(ruleItems[index]) - ruleKey = strings.TrimSpace(results[1]) - rulePattern = strings.TrimSpace(results[2]) + err error + match = false + results = ruleRegex.FindStringSubmatch(ruleItems[index]) + ruleKey = strings.TrimSpace(results[1]) + rulePattern = strings.TrimSpace(results[2]) + customRuleFunc RuleFunc ) if len(msgArray) > index { customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index]) } - if f, ok := customRuleFuncMap[ruleKey]; ok { + // Custom rule handling. + // 1. It firstly checks and uses the custom registered rules functions in the current Validator. + // 2. It secondly checks and uses the globally registered rules functions. + // 3. It finally checks and uses the build-in rules functions. + customRuleFunc = v.getRuleFunc(ruleKey) + if customRuleFunc != nil { // It checks custom validation rules with most priority. - var ( - dataMap map[string]interface{} - message = v.getErrorMessageByRule(ruleKey, customMsgMap) - ) - if len(paramMap) > 0 && paramMap[0] != nil { - dataMap = gconv.Map(paramMap[0]) - } - if err := f(v.ctx, ruleItems[index], value, message, dataMap); err != nil { + message := v.getErrorMessageByRule(ruleKey, customMsgMap) + if err := customRuleFunc(v.ctx, ruleItems[index], value, message, dataRaw); err != nil { match = false errorMsgArray[ruleKey] = err.Error() } else { @@ -112,7 +121,7 @@ func (v *Validator) doCheckValue(key string, value interface{}, rules string, me } } else { // It checks build-in validation rules if there's no custom rule. - match, err = v.doCheckBuildInRules(index, value, ruleKey, rulePattern, ruleItems, data, customMsgMap) + match, err = v.doCheckBuildInRules(index, value, ruleKey, rulePattern, ruleItems, dataMap, customMsgMap) if !match && err != nil { errorMsgArray[ruleKey] = err.Error() } diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index 423957391..d11fb4e9f 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -115,21 +115,6 @@ func ExampleCheckStruct3() { } func ExampleRegisterRule() { - rule := "unique-name" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error { - var ( - id = gconv.Int(params["Id"]) - name = gconv.String(value) - ) - n, err := g.Table("user").Where("id != ? and name = ?", id, name).Count() - if err != nil { - return err - } - if n > 0 { - return errors.New(message) - } - return nil - }) type User struct { Id int Name string `v:"required|unique-name # 请输入用户名称|用户名称已被占用"` @@ -140,6 +125,22 @@ func ExampleRegisterRule() { Name: "john", Pass: "123456", } + + rule := "unique-name" + gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + var ( + id = data.(*User).Id + name = gconv.String(value) + ) + n, err := g.Model("user").Where("id != ? and name = ?", id, name).Count() + if err != nil { + return err + } + if n > 0 { + return errors.New(message) + } + return nil + }) err := gvalid.CheckStruct(context.TODO(), user, nil) fmt.Println(err.Error()) // May Output: @@ -148,7 +149,7 @@ func ExampleRegisterRule() { func ExampleRegisterRule_OverwriteRequired() { rule := "required" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error { + gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { reflectValue := reflect.ValueOf(value) if reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index c9493c861..0a4e694b4 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -20,16 +20,20 @@ import ( func Test_CustomRule1(t *testing.T) { rule := "custom" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error { - pass := gconv.String(value) - if len(pass) != 6 { - return errors.New(message) - } - if params["data"] != pass { - return errors.New(message) - } - return nil - }) + err := gvalid.RegisterRule( + rule, + func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + pass := gconv.String(value) + if len(pass) != 6 { + return errors.New(message) + } + m := gconv.Map(data) + if m["data"] != pass { + return errors.New(message) + } + return nil + }, + ) gtest.Assert(err, nil) gtest.C(t, func(t *gtest.T) { err := gvalid.CheckValue(context.TODO(), "123456", rule, "custom message") @@ -67,7 +71,7 @@ func Test_CustomRule1(t *testing.T) { func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error { + err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { m := gconv.Map(value) if len(m) == 0 { return errors.New(message) @@ -111,7 +115,7 @@ func Test_CustomRule2(t *testing.T) { func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, params map[string]interface{}) error { + err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { s := gconv.String(value) if len(s) == 0 || s == "gf" { return nil @@ -153,3 +157,118 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { t.Assert(err.String(), "自定义错误") }) } + +func TestValidator_RuleFunc(t *testing.T) { + ruleName := "custom_1" + ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + pass := gconv.String(value) + if len(pass) != 6 { + return errors.New(message) + } + if m := gconv.Map(data); m["data"] != pass { + return errors.New(message) + } + return nil + } + gtest.C(t, func(t *gtest.T) { + err := g.Validator().Rules(ruleName).Messages("custom message").RuleFunc(ruleName, ruleFunc).CheckValue("123456") + t.Assert(err.String(), "custom message") + err = g.Validator(). + Rules(ruleName). + Messages("custom message"). + Data(g.Map{"data": "123456"}). + RuleFunc(ruleName, ruleFunc). + CheckValue("123456") + t.AssertNil(err) + }) + // Error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123", + Data: "123456", + } + err := g.Validator().RuleFunc(ruleName, ruleFunc).CheckStruct(st) + t.Assert(err.String(), "自定义错误") + }) + // No error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123456", + Data: "123456", + } + err := g.Validator().RuleFunc(ruleName, ruleFunc).CheckStruct(st) + t.AssertNil(err) + }) +} + +func TestValidator_RuleFuncMap(t *testing.T) { + ruleName := "custom_1" + ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + pass := gconv.String(value) + if len(pass) != 6 { + return errors.New(message) + } + if m := gconv.Map(data); m["data"] != pass { + return errors.New(message) + } + return nil + } + gtest.C(t, func(t *gtest.T) { + err := g.Validator(). + Rules(ruleName). + Messages("custom message"). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckValue("123456") + t.Assert(err.String(), "custom message") + err = g.Validator(). + Rules(ruleName). + Messages("custom message"). + Data(g.Map{"data": "123456"}). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }). + CheckValue("123456") + t.AssertNil(err) + }) + // Error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123", + Data: "123456", + } + err := g.Validator(). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckStruct(st) + t.Assert(err.String(), "自定义错误") + }) + // No error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123456", + Data: "123456", + } + err := g.Validator(). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckStruct(st) + t.AssertNil(err) + }) +}