From eaef2d865e47c6da07914940abc1fa86d9508878 Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 22 Nov 2021 14:41:33 +0800 Subject: [PATCH] add rule ci for package gvalid --- util/gvalid/gvalid.go | 239 ++++++++---------- util/gvalid/gvalid_validator.go | 8 + util/gvalid/gvalid_validator_check_value.go | 151 ++++++----- util/gvalid/gvalid_validator_rule_required.go | 57 +++-- .../gvalid_z_unit_feature_checkmap_test.go | 2 +- util/gvalid/gvalid_z_unit_feature_ci_test.go | 30 +++ 6 files changed, 261 insertions(+), 226 deletions(-) create mode 100644 util/gvalid/gvalid_z_unit_feature_ci_test.go diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index 5c358ee9f..d2b816459 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -15,56 +15,6 @@ import ( "github.com/gogf/gf/v2/text/gregex" ) -// 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. -// required-if format: required-if:field,value,... brief: Required unless all given field and its value are equal. -// required-unless format: required-unless:field,value,... brief: Required unless all given field and its value are not equal. -// required-with format: required-with:field1,field2,... brief: Required if any of given fields are not empty. -// required-with-all format: required-with-all:field1,field2,... brief: Required if all 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 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 -// datetime format: datetime brief: Standard datetime, like: 2006-01-02 12:00:00 -// date-format format: date-format:format brief: Custom date format. -// email format: email brief: Email address. -// phone format: phone brief: Phone number. -// telephone format: telephone brief: Telephone number, like: "XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" -// passport format: passport brief: Universal passport format rule: Starting with letter, containing only numbers or underscores, length between 6 and 18 -// password format: password brief: Universal password format rule1: Containing any visible chars, length between 6 and 18. -// password2 format: password2 brief: Universal password format rule2: Must meet password rule1, must contain lower and upper letters and numbers. -// password3 format: password3 brief: Universal password format rule3: Must meet password rule1, must contain lower and upper letters, numbers and special chars. -// postcode format: postcode brief: Postcode number. -// resident-id format: resident-id brief: Resident id number. -// bank-card format: bank-card brief: Bank card nunber. -// qq format: qq brief: Tencent QQ number. -// ip format: ip brief: IPv4/IPv6. -// ipv4 format: ipv4 brief: IPv4. -// ipv6 format: ipv6 brief: IPv6. -// mac format: mac brief: MAC. -// url format: url brief: URL. -// domain format: domain brief: Domain. -// length format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// min-length format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// max-length format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// size format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. -// between format: between:min,max brief: Range between :min and :max. It supports both integer and float. -// min format: min:min brief: Equal or greater than :min. It supports both integer and float. -// max format: max:max brief: Equal or lesser than :max. It supports both integer and float. -// json format: json brief: JSON. -// integer format: integer brief: Integer. -// float format: float brief: Float. Note that an integer is actually a float number. -// boolean format: boolean brief: Boolean(1,true,on,yes:true | 0,false,off,no,"":false) -// same format: same:field brief: Value should be the same as value of field. -// different format: different:field brief: Value should be different from value of field. -// in format: in:value1,value2,... brief: Value should be in: value1,value2,... -// not-in format: not-in:value1,value2,... brief: Value should not be in: value1,value2,... -// regex format: regex:pattern brief: Value should match custom regular expression pattern. - // CustomMsg is the custom error message type, // like: map[field] => string|map[rule]string type CustomMsg = map[string]interface{} @@ -89,100 +39,64 @@ 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" + ruleNameBail = "bail" // the name for rule "bail" + ruleNameCi = "ci" // the name for rule "ci" ) var ( - defaultValidator = New() // defaultValidator is the default validator for package functions. - structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. - aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. - - // all internal error keys. - internalErrKeyMap = map[string]string{ - internalRulesErrRuleName: internalRulesErrRuleName, - internalParamsErrRuleName: internalParamsErrRuleName, - internalObjectErrRuleName: internalObjectErrRuleName, - } - // regular expression object for single rule - // which is compiled just once and of repeatable usage. - ruleRegex, _ = regexp.Compile(singleRulePattern) - - // mustCheckRulesEvenValueEmpty specifies some rules that must be validated - // even the value is empty (nil or empty). - mustCheckRulesEvenValueEmpty = map[string]struct{}{ - "required": {}, - "required-if": {}, - "required-unless": {}, - "required-with": {}, - "required-with-all": {}, - "required-without": {}, - "required-without-all": {}, - //"same": {}, - //"different": {}, - //"in": {}, - //"not-in": {}, - //"regex": {}, - } // allSupportedRules defines all supported rules that is used for quick checks. + // Refer to Laravel validation: + // https://laravel.com/docs/5.5/validation#available-validation-rules + // https://learnku.com/docs/laravel/5.4/validation allSupportedRules = map[string]struct{}{ - "required": {}, - "required-if": {}, - "required-unless": {}, - "required-with": {}, - "required-with-all": {}, - "required-without": {}, - "required-without-all": {}, - "bail": {}, - "date": {}, - "datetime": {}, - "date-format": {}, - "email": {}, - "phone": {}, - "phone-loose": {}, - "telephone": {}, - "passport": {}, - "password": {}, - "password2": {}, - "password3": {}, - "postcode": {}, - "resident-id": {}, - "bank-card": {}, - "qq": {}, - "ip": {}, - "ipv4": {}, - "ipv6": {}, - "mac": {}, - "url": {}, - "domain": {}, - "length": {}, - "min-length": {}, - "max-length": {}, - "size": {}, - "between": {}, - "min": {}, - "max": {}, - "json": {}, - "integer": {}, - "float": {}, - "boolean": {}, - "same": {}, - "different": {}, - "in": {}, - "not-in": {}, - "regex": {}, - } - // boolMap defines the boolean values. - boolMap = map[string]struct{}{ - "1": {}, - "true": {}, - "on": {}, - "yes": {}, - "": {}, - "0": {}, - "false": {}, - "off": {}, - "no": {}, + "required": {}, // format: required brief: Required. + "required-if": {}, // format: required-if:field,value,... brief: Required unless all given field and its value are equal. + "required-unless": {}, // format: required-unless:field,value,... brief: Required unless all given field and its value are not equal. + "required-with": {}, // format: required-with:field1,field2,... brief: Required if any of given fields are not empty. + "required-with-all": {}, // format: required-with-all:field1,field2,... brief: Required if all 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 given fields are empty. + "bail": {}, // format: bail brief: Stop validating when this field's validation failed. + "ci": {}, // format: ci brief: Case-Insensitive configuration for those rules that need value comparison like: same, different, in, not-in, etc. + "date": {}, // format: date brief: Standard date, like: 2006-01-02, 20060102, 2006.01.02 + "datetime": {}, // format: datetime brief: Standard datetime, like: 2006-01-02 12:00:00 + "date-format": {}, // format: date-format:format brief: Custom date format. + "email": {}, // format: email brief: Email address. + "phone": {}, // format: phone brief: Phone number. + "phone-loose": {}, // format: phone-loose brief: Loose phone number validation. + "telephone": {}, // format: telephone brief: Telephone number, like: "XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" + "passport": {}, // format: passport brief: Universal passport format rule: Starting with letter, containing only numbers or underscores, length between 6 and 18 + "password": {}, // format: password brief: Universal password format rule1: Containing any visible chars, length between 6 and 18. + "password2": {}, // format: password2 brief: Universal password format rule2: Must meet password rule1, must contain lower and upper letters and numbers. + "password3": {}, // format: password3 brief: Universal password format rule3: Must meet password rule1, must contain lower and upper letters, numbers and special chars. + "postcode": {}, // format: postcode brief: Postcode number. + "resident-id": {}, // format: resident-id brief: Resident id number. + "bank-card": {}, // format: bank-card brief: Bank card number. + "qq": {}, // format: qq brief: Tencent QQ number. + "ip": {}, // format: ip brief: IPv4/IPv6. + "ipv4": {}, // format: ipv4 brief: IPv4. + "ipv6": {}, // format: ipv6 brief: IPv6. + "mac": {}, // format: mac brief: MAC. + "url": {}, // format: url brief: URL. + "domain": {}, // format: domain brief: Domain. + "length": {}, // format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "min-length": {}, // format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "max-length": {}, // format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "size": {}, // format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. + "between": {}, // format: between:min,max brief: Range between :min and :max. It supports both integer and float. + "min": {}, // format: min:min brief: Equal or greater than :min. It supports both integer and float. + "max": {}, // format: max:max brief: Equal or lesser than :max. It supports both integer and float. + "json": {}, // format: json brief: JSON. + "integer": {}, // format: integer brief: Integer. + "float": {}, // format: float brief: Float. Note that an integer is actually a float number. + "boolean": {}, // format: boolean brief: Boolean(1,true,on,yes:true | 0,false,off,no,"":false) + "same": {}, // format: same:field brief: Value should be the same as value of field. + "different": {}, // format: different:field brief: Value should be different from value of field. + "in": {}, // format: in:value1,value2,... brief: Value should be in: value1,value2,... + "not-in": {}, // format: not-in:value1,value2,... brief: Value should not be in: value1,value2,... + "regex": {}, // format: regex:pattern brief: Value should match custom regular expression pattern. } + // defaultMessages is the default error messages. // Note that these messages are synchronized from ./i18n/en/validation.toml . defaultMessages = map[string]string{ @@ -232,11 +146,56 @@ var ( "regex": "The {attribute} value `{value}` must be in regex of: {pattern}", internalDefaultRuleName: "The {attribute} value `{value}` is invalid", } + + // mustCheckRulesEvenValueEmpty specifies some rules that must be validated + // even the value is empty (nil or empty). + mustCheckRulesEvenValueEmpty = map[string]struct{}{ + "required": {}, + "required-if": {}, + "required-unless": {}, + "required-with": {}, + "required-with-all": {}, + "required-without": {}, + "required-without-all": {}, + //"same": {}, + //"different": {}, + //"in": {}, + //"not-in": {}, + //"regex": {}, + } + + // boolMap defines the boolean values. + boolMap = map[string]struct{}{ + "1": {}, + "true": {}, + "on": {}, + "yes": {}, + "": {}, + "0": {}, + "false": {}, + "off": {}, + "no": {}, + } + + defaultValidator = New() // defaultValidator is the default validator for package functions. + structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. + aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. + + // all internal error keys. + internalErrKeyMap = map[string]string{ + internalRulesErrRuleName: internalRulesErrRuleName, + internalParamsErrRuleName: internalParamsErrRuleName, + internalObjectErrRuleName: internalObjectErrRuleName, + } + // regular expression object for single rule + // which is compiled just once and of repeatable usage. + ruleRegex, _ = regexp.Compile(singleRulePattern) + // 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, + ruleNameBail: true, + ruleNameCi: true, } ) diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 33772534c..0057275b3 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -21,6 +21,7 @@ type Validator struct { 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`. bail bool // Stop validation after the first validation error. + caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison. } // New creates and returns a new Validator. @@ -52,6 +53,13 @@ func (v *Validator) Bail() *Validator { return newValidator } +// CaseInsensitive sets the mark for Case-Insensitive for those rules that need value comparison. +func (v *Validator) CaseInsensitive() *Validator { + newValidator := v.Clone() + newValidator.caseInsensitive = true + return newValidator +} + // Data is a chaining operation function, which sets validation data for current operation. // The parameter `data` is usually type of map, which specifies the parameter map used in validation. // Calling this function also sets `useDataInsteadOfObjectAttributes` true no mather the `data` is nil or not. diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 0cfa78fcc..c99eef68c 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -54,9 +54,9 @@ type doCheckValueInput struct { } // doCheckSingleValue does the really rules validation for single key-value. -func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) Error { +func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Error { // If there's no validation rules, it does nothing and returns quickly. - if input.Rule == "" { + if in.Rule == "" { return nil } // It converts value to string and then does the validation. @@ -69,17 +69,17 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E msgArray = make([]string, 0) customMsgMap = make(map[string]string) ) - switch messages := input.Messages.(type) { + switch messages := in.Messages.(type) { case string: msgArray = strings.Split(messages, "|") default: - for k, message := range gconv.Map(input.Messages) { + for k, message := range gconv.Map(in.Messages) { customMsgMap[k] = gconv.String(message) } } // Handle the char '|' in the rule, // which makes this rule separated into multiple rules. - ruleItems := strings.Split(strings.TrimSpace(input.Rule), "|") + ruleItems := strings.Split(strings.TrimSpace(in.Rule), "|") for i := 0; ; { array := strings.Split(ruleItems[i], ":") _, ok := allSupportedRules[array[0]] @@ -90,7 +90,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } else { return newValidationErrorByStr( internalRulesErrRuleName, - errors.New(internalRulesErrRuleName+": "+input.Rule), + errors.New(internalRulesErrRuleName+": "+in.Rule), ) } } else { @@ -101,7 +101,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } } var ( - hasBailRule = false + hasBailRule = v.bail + hasCaseInsensitive = v.caseInsensitive ) for index := 0; index < len(ruleItems); { var ( @@ -113,10 +114,14 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E customRuleFunc RuleFunc ) - if !hasBailRule && ruleKey == bailRuleName { + if !hasBailRule && ruleKey == ruleNameBail { hasBailRule = true } + if !hasCaseInsensitive && ruleKey == ruleNameCi { + hasCaseInsensitive = true + } + // Ignore logic executing for marked rules. if markedRuleMap[ruleKey] { index++ @@ -138,8 +143,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if err = customRuleFunc(ctx, RuleFuncInput{ Rule: ruleItems[index], Message: message, - Value: gvar.New(input.Value), - Data: gvar.New(input.DataRaw), + Value: gvar.New(in.Value), + Data: gvar.New(in.DataRaw), }); err != nil { match = false // The error should have stack info to indicate the error position. @@ -161,13 +166,14 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E match, err = v.doCheckSingleBuildInRules( ctx, doCheckBuildInRulesInput{ - Index: index, - Value: input.Value, - RuleKey: ruleKey, - RulePattern: rulePattern, - RuleItems: ruleItems, - DataMap: input.DataMap, - CustomMsgMap: customMsgMap, + Index: index, + Value: in.Value, + RuleKey: ruleKey, + RulePattern: rulePattern, + RuleItems: ruleItems, + DataMap: in.DataMap, + CustomMsgMap: customMsgMap, + CaseInsensitive: hasCaseInsensitive, }, ) if !match && err != nil { @@ -187,9 +193,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if err = ruleErrorMap[ruleKey]; !gerror.HasStack(err) { var s string s = gstr.ReplaceByMap(err.Error(), map[string]string{ - "{value}": gconv.String(input.Value), + "{value}": gconv.String(in.Value), "{pattern}": rulePattern, - "{attribute}": input.Name, + "{attribute}": in.Name, }) s, _ = gregex.ReplaceString(`\s{2,}`, ` `, s) ruleErrorMap[ruleKey] = errors.New(s) @@ -206,9 +212,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if len(ruleErrorMap) > 0 { return newValidationError( gcode.CodeValidationFailed, - []fieldRule{{Name: input.Name, Rule: input.Rule}}, + []fieldRule{{Name: in.Name, Rule: in.Rule}}, map[string]map[string]error{ - input.Name: ruleErrorMap, + in.Name: ruleErrorMap, }, ) } @@ -216,18 +222,19 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } type doCheckBuildInRulesInput struct { - Index int // Index of RuleKey in RuleItems. - Value interface{} // Value to be validated. - RuleKey string // RuleKey is like the "max" in rule "max: 6" - RulePattern string // RulePattern is like "6" in rule:"max:6" - RuleItems []string // RuleItems are all the rules that should be validated on single field, like: []string{"required", "min:1"} - DataMap map[string]interface{} // Parameter map. - CustomMsgMap map[string]string // Custom error message map. + Index int // Index of RuleKey in RuleItems. + Value interface{} // Value to be validated. + RuleKey string // RuleKey is like the "max" in rule "max: 6" + RulePattern string // RulePattern is like "6" in rule:"max:6" + RuleItems []string // RuleItems are all the rules that should be validated on single field, like: []string{"required", "min:1"} + DataMap map[string]interface{} // Parameter map. + CustomMsgMap map[string]string // Custom error message map. + CaseInsensitive bool // Case-Insensitive comparison. } -func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheckBuildInRulesInput) (match bool, err error) { - valueStr := gconv.String(input.Value) - switch input.RuleKey { +func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBuildInRulesInput) (match bool, err error) { + valueStr := gconv.String(in.Value) + switch in.RuleKey { // Required rules. case "required", @@ -237,7 +244,13 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "required-with-all", "required-without", "required-without-all": - match = v.checkRequired(input.Value, input.RuleKey, input.RulePattern, input.DataMap) + match = v.checkRequired(checkRequiredInput{ + Value: in.Value, + RuleKey: in.RuleKey, + RulePattern: in.RulePattern, + DataMap: in.DataMap, + CaseInsensitive: in.CaseInsensitive, + }) // Length rules. // It also supports length of unicode string. @@ -246,7 +259,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "min-length", "max-length", "size": - if msg := v.checkLength(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { + if msg := v.checkLength(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" { return match, errors.New(msg) } else { match = true @@ -257,7 +270,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck "min", "max", "between": - if msg := v.checkRange(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { + if msg := v.checkRange(ctx, valueStr, in.RuleKey, in.RulePattern, in.CustomMsgMap); msg != "" { return match, errors.New(msg) } else { match = true @@ -266,27 +279,27 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Custom regular expression. case "regex": // It here should check the rule as there might be special char '|' in it. - for i := input.Index + 1; i < len(input.RuleItems); i++ { - if !gregex.IsMatchString(singleRulePattern, input.RuleItems[i]) { - input.RulePattern += "|" + input.RuleItems[i] - input.Index++ + for i := in.Index + 1; i < len(in.RuleItems); i++ { + if !gregex.IsMatchString(singleRulePattern, in.RuleItems[i]) { + in.RulePattern += "|" + in.RuleItems[i] + in.Index++ } } - match = gregex.IsMatchString(input.RulePattern, valueStr) + match = gregex.IsMatchString(in.RulePattern, valueStr) // Date rules. case "date": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } match = gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr) // Datetime rule. case "datetime": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } if _, err = gtime.StrToTimeFormat(valueStr, `Y-m-d H:i:s`); err == nil { match = true @@ -295,54 +308,61 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Date rule with specified format. case "date-format": // support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time. - if v, ok := input.Value.(iTime); ok { - return !v.IsZero(), nil + if value, ok := in.Value.(iTime); ok { + return !value.IsZero(), nil } - if _, err = gtime.StrToTimeFormat(valueStr, input.RulePattern); err == nil { + if _, err = gtime.StrToTimeFormat(valueStr, in.RulePattern); err == nil { match = true } else { var ( msg string ) - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Values of two fields should be equal as string. case "same": - _, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern) + _, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern) if foundValue != nil { - if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { - match = true + if in.CaseInsensitive { + match = strings.EqualFold(valueStr, gconv.String(foundValue)) + } else { + match = strings.Compare(valueStr, gconv.String(foundValue)) == 0 } } if !match { var msg string - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Values of two fields should not be equal as string. case "different": match = true - _, foundValue := gutil.MapPossibleItemByKey(input.DataMap, input.RulePattern) + _, foundValue := gutil.MapPossibleItemByKey(in.DataMap, in.RulePattern) if foundValue != nil { - if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { - match = false + if in.CaseInsensitive { + match = !strings.EqualFold(valueStr, gconv.String(foundValue)) + } else { + match = strings.Compare(valueStr, gconv.String(foundValue)) != 0 } } if !match { var msg string - msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) + msg = v.getErrorMessageByRule(ctx, in.RuleKey, in.CustomMsgMap) return match, errors.New(msg) } // Field value should be in range of. case "in": - array := gstr.SplitAndTrim(input.RulePattern, ",") - for _, v := range array { - if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 { - match = true + for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") { + if in.CaseInsensitive { + match = strings.EqualFold(valueStr, strings.TrimSpace(value)) + } else { + match = strings.Compare(valueStr, strings.TrimSpace(value)) == 0 + } + if match { break } } @@ -350,10 +370,13 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck // Field value should not be in range of. case "not-in": match = true - array := gstr.SplitAndTrim(input.RulePattern, ",") - for _, v := range array { - if strings.Compare(valueStr, strings.TrimSpace(v)) == 0 { - match = false + for _, value := range gstr.SplitAndTrim(in.RulePattern, ",") { + if in.CaseInsensitive { + match = !strings.EqualFold(valueStr, strings.TrimSpace(value)) + } else { + match = strings.Compare(valueStr, strings.TrimSpace(value)) != 0 + } + if !match { break } } @@ -517,7 +540,7 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, input doCheck match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr) default: - return match, errors.New("Invalid rule name: " + input.RuleKey) + return match, errors.New("Invalid rule name: " + in.RuleKey) } return match, nil } diff --git a/util/gvalid/gvalid_validator_rule_required.go b/util/gvalid/gvalid_validator_rule_required.go index 06de0c50d..c09dedf24 100644 --- a/util/gvalid/gvalid_validator_rule_required.go +++ b/util/gvalid/gvalid_validator_rule_required.go @@ -15,11 +15,19 @@ import ( "github.com/gogf/gf/v2/util/gutil" ) +type checkRequiredInput struct { + Value interface{} // Value to be validated. + RuleKey string // RuleKey is like the "max" in rule "max: 6" + RulePattern string // RulePattern is like "6" in rule:"max:6" + DataMap map[string]interface{} // Parameter map. + CaseInsensitive bool // Case-Insensitive comparison. +} + // checkRequired checks `value` using required rules. // It also supports require checks for `value` of type: slice, map. -func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string, dataMap map[string]interface{}) bool { +func (v *Validator) checkRequired(in checkRequiredInput) bool { required := false - switch ruleKey { + switch in.RuleKey { // Required. case "required": required = true @@ -29,7 +37,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-if": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) // It supports multiple field and value pairs. @@ -37,9 +45,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) - if strings.Compare(tv, gconv.String(foundValue)) == 0 { - required = true + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, tk) + if in.CaseInsensitive { + required = strings.EqualFold(tv, gconv.String(foundValue)) + } else { + required = strings.Compare(tv, gconv.String(foundValue)) == 0 + } + if required { break } i += 2 @@ -51,7 +63,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-unless": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) // It supports multiple field and value pairs. @@ -59,12 +71,15 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) - if strings.Compare(tv, gconv.String(foundValue)) == 0 { - required = false + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, tk) + if in.CaseInsensitive { + required = !strings.EqualFold(tv, gconv.String(foundValue)) + } else { + required = strings.Compare(tv, gconv.String(foundValue)) != 0 + } + if !required { break } - i += 2 } } @@ -74,11 +89,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-with": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if !empty.IsEmpty(foundValue) { required = true break @@ -90,11 +105,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-with-all": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if empty.IsEmpty(foundValue) { required = false break @@ -106,11 +121,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-without": required = false var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if empty.IsEmpty(foundValue) { required = true break @@ -122,11 +137,11 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case "required-without-all": required = true var ( - array = strings.Split(rulePattern, ",") + array = strings.Split(in.RulePattern, ",") foundValue interface{} ) for i := 0; i < len(array); i++ { - _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + _, foundValue = gutil.MapPossibleItemByKey(in.DataMap, array[i]) if !empty.IsEmpty(foundValue) { required = false break @@ -134,7 +149,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string } } if required { - reflectValue := reflect.ValueOf(value) + reflectValue := reflect.ValueOf(in.Value) for reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() } @@ -142,7 +157,7 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string case reflect.String, reflect.Map, reflect.Array, reflect.Slice: return reflectValue.Len() != 0 } - return gconv.String(value) != "" + return gconv.String(in.Value) != "" } else { return true } diff --git a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go index 4f0849a76..d7c77cbc8 100755 --- a/util/gvalid/gvalid_z_unit_feature_checkmap_test.go +++ b/util/gvalid/gvalid_z_unit_feature_checkmap_test.go @@ -226,7 +226,7 @@ func Test_Map_Bail(t *testing.T) { } err := g.Validator().Bail().Rules(rules).CheckMap(ctx, params) t.AssertNE(err, nil) - t.Assert(err.String(), "账号不能为空; 账号长度应当在6到16之间") + t.Assert(err.String(), "账号不能为空") }) // global bail with rule bail gtest.C(t, func(t *gtest.T) { diff --git a/util/gvalid/gvalid_z_unit_feature_ci_test.go b/util/gvalid/gvalid_z_unit_feature_ci_test.go new file mode 100644 index 000000000..a2600110c --- /dev/null +++ b/util/gvalid/gvalid_z_unit_feature_ci_test.go @@ -0,0 +1,30 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gvalid_test + +import ( + "testing" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gvalid" +) + +func Test_CI(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + err := gvalid.CheckValue(ctx, "id", "in:Id,Name", nil) + t.AssertNE(err, nil) + }) + gtest.C(t, func(t *gtest.T) { + err := gvalid.CheckValue(ctx, "id", "ci|in:Id,Name", nil) + t.AssertNil(err) + }) + gtest.C(t, func(t *gtest.T) { + err := g.Validator().CaseInsensitive().Rules("in:Id,Name").CheckValue(ctx, "id") + t.AssertNil(err) + }) +}