add rule ci for package gvalid

This commit is contained in:
John Guo 2021-11-22 14:41:33 +08:00
parent f5920fff68
commit eaef2d865e
6 changed files with 261 additions and 226 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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