add custom rule fucntions feature for package gvalid

This commit is contained in:
John Guo
2021-05-29 11:30:34 +08:00
parent 5100e0e8b7
commit fa1814ff54
9 changed files with 236 additions and 74 deletions

View File

@ -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) {

View File

@ -1 +1,3 @@
OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。"
OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。"
hello = "你好"
world = "世界"

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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()

View File

@ -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)
})
}