add bail rule for package gvalid

This commit is contained in:
John Guo
2021-08-01 22:12:44 +08:00
parent fa64df6f91
commit 7678540270
4 changed files with 114 additions and 26 deletions

View File

@ -15,7 +15,9 @@ import (
"github.com/gogf/gf/text/gregex"
)
// Refer to Laravel validation: https://laravel.com/docs/5.5/validation#available-validation-rules
// Refer to Laravel validation:
// https://laravel.com/docs/5.5/validation#available-validation-rules
// https://learnku.com/docs/laravel/5.4/validation
//
// All supported rules:
// required format: required brief: Required.
@ -25,6 +27,7 @@ import (
// required-with-all format: required-with-all:field1,field2,... brief: Required if all of given fields are not empty.
// required-without format: required-without:field1,field2,... brief: Required if any of given fields are empty.
// required-without-all format: required-without-all:field1,field2,...brief: Required if all of given fields are empty.
// bail format: bail brief: Stop validating when this field's validation failed.
// date format: date brief: Standard date, like: 2006-01-02, 20060102, 2006.01.02
// date-format format: date-format:format brief: Custom date format.
// email format: email brief: Email address.
@ -79,6 +82,7 @@ const (
internalDefaultRuleName = "__default__" // default rule name for i18n error message format if no i18n message found for specified error rule.
ruleMessagePrefixForI18n = "gf.gvalid.rule." // prefix string for each rule configuration in i18n content.
noValidationTagName = "nv" // no validation tag name for struct attribute.
bailRuleName = "bail" // the name for rule "bail"
)
var (
@ -121,6 +125,7 @@ var (
"required-with-all": {},
"required-without": {},
"required-without-all": {},
"bail": {},
"date": {},
"date-format": {},
"email": {},
@ -219,6 +224,12 @@ var (
"regex": "The :attribute value is invalid",
internalDefaultRuleName: "The :attribute value is invalid",
}
// markedRuleMap defines all rules that are just marked rules which have neither functional meaning
// nor error messages.
markedRuleMap = map[string]bool{
bailRuleName: true,
"nullable": true,
}
)
// CheckValue checks single value with specified rules.

View File

@ -62,11 +62,11 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
}
var (
inputParamMap map[string]interface{}
checkRules = make(map[string]string)
customMessage = make(CustomMsg)
checkValueData = v.data
errorRules = make([]string, 0) // Sequence rules.
inputParamMap map[string]interface{}
checkRuleStrMap = make(map[string]string) // Complete rules map of struct: map[name]rule, the rule is complete pattern like: Name@RuleStr#Message
customMessage = make(CustomMsg) // Custom rule error message map.
checkValueData = v.data // Ready to be validated data, which can be type of .
errorRules = make([]string, 0) // Sequence rules.
)
if checkValueData == nil {
checkValueData = object
@ -101,17 +101,17 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
customMessage[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k])
}
}
checkRules[name] = rule
checkRuleStrMap[name] = rule
errorRules = append(errorRules, name+"@"+rule)
}
// Map type rules does not support sequence.
// Format: map[key]rule
case map[string]string:
checkRules = v
checkRuleStrMap = v
}
// If there's no struct tag and validation rules, it does nothing and returns quickly.
if len(tagField) == 0 && len(checkRules) == 0 {
if len(tagField) == 0 && len(checkRuleStrMap) == 0 {
return nil
}
// Input parameter map handling.
@ -161,14 +161,14 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
}
}
}
if _, ok := checkRules[name]; !ok {
if _, ok := checkRules[fieldName]; ok {
if _, ok := checkRuleStrMap[name]; !ok {
if _, ok := checkRuleStrMap[fieldName]; ok {
// If there's alias name,
// use alias name as its key and remove the field name key.
checkRules[name] = checkRules[fieldName]
delete(checkRules, fieldName)
checkRuleStrMap[name] = checkRuleStrMap[fieldName]
delete(checkRuleStrMap, fieldName)
} else {
checkRules[name] = rule
checkRuleStrMap[name] = rule
}
errorRules = append(errorRules, name+"@"+rule)
} else {
@ -211,13 +211,16 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
}
}
// The following logic is the same as some of CheckMap.
var value interface{}
for key, rule := range checkRules {
// The following logic is the same as some of CheckMap but with sequence support.
var (
value interface{}
hasBailRule bool
)
for key, rule := range checkRuleStrMap {
_, value = gutil.MapPossibleItemByKey(inputParamMap, key)
// It checks each rule and its value in loop.
if e := v.doCheckValue(key, value, rule, customMessage[key], checkValueData, inputParamMap); e != nil {
_, item := e.FirstItem()
if validatedError := v.doCheckValue(key, value, rule, customMessage[key], checkValueData, inputParamMap); validatedError != nil {
_, item := validatedError.FirstItem()
// ===================================================================
// Only in map and struct validations, if value is nil or empty string
// and has no required* rules, it clears the error message.
@ -247,6 +250,9 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
for k, v := range item {
errorMaps[key][k] = v
}
if hasBailRule {
break
}
}
}
if len(errorMaps) > 0 {

View File

@ -34,13 +34,14 @@ func (v *Validator) CheckValue(value interface{}) Error {
// doCheckSingleValue does the really rules validation for single key-value.
//
// The parameter `rules` specifies the validation rules string, like "required", "required|between:1,100", etc.
// The parameter `name` specifies the name of parameter `value`.
// The parameter `value` specifies the value for this rules to be validated.
// The parameter `rules` specifies the validation rules string, like "required", "required|between:1,100", etc.
// 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,
name string,
value interface{},
rules string,
messages interface{},
@ -92,15 +93,29 @@ func (v *Validator) doCheckValue(
break
}
}
var (
hasBailRule = false
)
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])
match = false // whether this rule is matched(has no error)
results = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule.
ruleKey = strings.TrimSpace(results[1]) // rule name like "max" in rule "max: 6"
rulePattern = strings.TrimSpace(results[2]) // rule value if any like "6" in rule:"max:6"
customRuleFunc RuleFunc
)
if !hasBailRule && ruleKey == bailRuleName {
hasBailRule = true
}
// Ignore logic executing for marked rules.
if markedRuleMap[ruleKey] {
index++
continue
}
if len(msgArray) > index {
customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index])
}
@ -134,12 +149,17 @@ func (v *Validator) doCheckValue(
if _, ok := errorMsgArray[ruleKey]; !ok {
errorMsgArray[ruleKey] = v.getErrorMessageByRule(ruleKey, customMsgMap)
}
// If it is with error and there's bail rule,
// it then does not continue validating for left rules.
if hasBailRule {
break
}
}
index++
}
if len(errorMsgArray) > 0 {
return newError(gerror.CodeValidationFailed, []string{rules}, map[string]map[string]string{
key: errorMsgArray,
name: errorMsgArray,
})
}
return nil

View File

@ -1027,3 +1027,54 @@ func Test_Code(t *testing.T) {
t.Assert(gerror.Code(err), gerror.CodeInternalError)
})
}
func Test_Bail(t *testing.T) {
// check value with no bail
gtest.C(t, func(t *gtest.T) {
err := g.Validator().
Rules("required|min:1|between:1,100").
Messages("|min number is 1|size is between 1 and 100").
CheckValue(-1)
t.AssertNE(err, nil)
t.Assert(err.Error(), "min number is 1; size is between 1 and 100")
})
// check value with bail
gtest.C(t, func(t *gtest.T) {
err := g.Validator().
Rules("bail|required|min:1|between:1,100").
Messages("||min number is 1|size is between 1 and 100").
CheckValue(-1)
t.AssertNE(err, nil)
t.Assert(err.Error(), "min number is 1")
})
// struct with no bail
gtest.C(t, func(t *gtest.T) {
type Params struct {
Page int `v:"required|min:1"`
Size int `v:"required|min:1|between:1,100 # |min number is 1|size is between 1 and 100"`
}
obj := &Params{
Page: 1,
Size: -1,
}
err := g.Validator().CheckStruct(obj)
t.AssertNE(err, nil)
t.Assert(err.Error(), "min number is 1; size is between 1 and 100")
})
// struct with bail
gtest.C(t, func(t *gtest.T) {
type Params struct {
Page int `v:"required|min:1"`
Size int `v:"bail|required|min:1|between:1,100 # ||min number is 1|size is between 1 and 100"`
}
obj := &Params{
Page: 1,
Size: -1,
}
err := g.Validator().CheckStruct(obj)
t.AssertNE(err, nil)
t.Assert(err.Error(), "min number is 1")
})
}