From 76785402709e4bf013655531271b632f800e1fc0 Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 1 Aug 2021 22:12:44 +0800 Subject: [PATCH] add bail rule for package gvalid --- util/gvalid/gvalid.go | 13 ++++- util/gvalid/gvalid_validator_check_struct.go | 42 +++++++++------- util/gvalid/gvalid_validator_check_value.go | 34 ++++++++++--- util/gvalid/gvalid_z_unit_basic_all_test.go | 51 ++++++++++++++++++++ 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index 5b390a125..834d6a82c 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -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. diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 868312744..50822e89e 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -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 { diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 78377d32d..ff5c507f0 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -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 diff --git a/util/gvalid/gvalid_z_unit_basic_all_test.go b/util/gvalid/gvalid_z_unit_basic_all_test.go index b263d1e9e..2997913d2 100755 --- a/util/gvalid/gvalid_z_unit_basic_all_test.go +++ b/util/gvalid/gvalid_z_unit_basic_all_test.go @@ -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") + }) +}