// 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 import ( "context" "errors" "reflect" "strings" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gregex" "github.com/gogf/gf/v2/text/gstr" "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gvalid/internal/builtin" ) type doCheckValueInput struct { Name string // Name specifies the name of parameter `value`, which might be the custom tag name of the parameter. Value interface{} // Value specifies the value for the rules to be validated. ValueType reflect.Type // ValueType specifies the type of the value, mainly used for value type id retrieving. Rule string // Rule specifies the validation rules string, like "required", "required|between:1,100", etc. Messages interface{} // Messages specifies the custom error messages for this rule from parameters input, which is usually type of map/slice. DataRaw interface{} // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value. DataMap map[string]interface{} // DataMap specifies the map that is converted from `dataRaw`. It is usually used internally } // doCheckValue does the really rules validation for single key-value. func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Error { // If there's no validation rules, it does nothing and returns quickly. if in.Rule == "" { return nil } // It converts value to string and then does the validation. var ( // Do not trim it as the space is also part of the value. ruleErrorMap = make(map[string]error) ) // Custom error messages handling. var ( msgArray = make([]string, 0) customMsgMap = make(map[string]string) ) switch messages := in.Messages.(type) { case string: msgArray = strings.Split(messages, "|") default: 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(in.Rule), "|") for i := 0; ; { array := strings.Split(ruleItems[i], ":") if builtin.GetRule(array[0]) == nil && v.getCustomRuleFunc(array[0]) == nil { // ============================ SPECIAL ============================ // Special `regex` and `not-regex` rules. // Merge the regex pattern if there are special chars, like ':', '|', in pattern. // ============================ SPECIAL ============================ var ( ruleNameRegexLengthMatch bool ruleNameNotRegexLengthMatch bool ) if i > 0 { ruleItem := ruleItems[i-1] if len(ruleItem) >= len(ruleNameRegex) && ruleItem[:len(ruleNameRegex)] == ruleNameRegex { ruleNameRegexLengthMatch = true } if len(ruleItem) >= len(ruleNameNotRegex) && ruleItem[:len(ruleNameNotRegex)] == ruleNameNotRegex { ruleNameNotRegexLengthMatch = true } } if i > 0 && (ruleNameRegexLengthMatch || ruleNameNotRegexLengthMatch) { ruleItems[i-1] += "|" + ruleItems[i] ruleItems = append(ruleItems[:i], ruleItems[i+1:]...) } else { return newValidationErrorByStr( internalRulesErrRuleName, errors.New(internalRulesErrRuleName+": "+ruleItems[i]), ) } } else { i++ } if i == len(ruleItems) { break } } var ( hasBailRule = v.bail hasForeachRule = v.foreach hasCaseInsensitive = v.caseInsensitive ) for index := 0; index < len(ruleItems); { var ( err error results = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule. ruleKey = gstr.Trim(results[1]) // rule key like "max" in rule "max: 6" rulePattern = gstr.Trim(results[2]) // rule pattern is like "6" in rule:"max:6" ) if !hasBailRule && ruleKey == ruleNameBail { hasBailRule = true } if !hasForeachRule && ruleKey == ruleNameForeach { hasForeachRule = true } if !hasCaseInsensitive && ruleKey == ruleNameCi { hasCaseInsensitive = true } // Ignore logic executing for marked rules. if decorativeRuleMap[ruleKey] { index++ continue } if len(msgArray) > index { customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index]) } var ( message = v.getErrorMessageByRule(ctx, ruleKey, customMsgMap) customRuleFunc = v.getCustomRuleFunc(ruleKey) builtinRule = builtin.GetRule(ruleKey) foreachValues = []interface{}{in.Value} ) if hasForeachRule { // As it marks `foreach`, so it converts the value to slice. foreachValues = gconv.Interfaces(in.Value) // Reset `foreach` rule as it only takes effect just once for next rule. hasForeachRule = false } for _, value := range foreachValues { switch { // Custom validation rules. case customRuleFunc != nil: err = customRuleFunc(ctx, RuleFuncInput{ Rule: ruleItems[index], Message: message, Field: in.Name, ValueType: in.ValueType, Value: gvar.New(value), Data: gvar.New(in.DataRaw), }) // Builtin validation rules. case customRuleFunc == nil && builtinRule != nil: err = builtinRule.Run(builtin.RunInput{ RuleKey: ruleKey, RulePattern: rulePattern, Field: in.Name, ValueType: in.ValueType, Value: gvar.New(value), Data: gvar.New(in.DataRaw), Message: message, Option: builtin.RunOption{ CaseInsensitive: hasCaseInsensitive, }, }) default: // It never comes across here. } // Error handling. if err != nil { // Error variable replacement for error message. if errMsg := err.Error(); gstr.Contains(errMsg, "{") { errMsg = gstr.ReplaceByMap(errMsg, map[string]string{ "{field}": in.Name, // Field name of the `value`. "{value}": gconv.String(value), // Current validating value. "{pattern}": rulePattern, // The variable part of the rule. "{attribute}": in.Name, // The same as `{field}`. It is deprecated. }) errMsg, _ = gregex.ReplaceString(`\s{2,}`, ` `, errMsg) err = errors.New(errMsg) } // The error should have stack info to indicate the error position. if !gerror.HasStack(err) { err = gerror.NewCode(gcode.CodeValidationFailed, err.Error()) } // The error should have error code that is `gcode.CodeValidationFailed`. if gerror.Code(err) == gcode.CodeNil { // TODO it's better using interface? if e, ok := err.(*gerror.Error); ok { e.SetCode(gcode.CodeValidationFailed) } } ruleErrorMap[ruleKey] = err // If it is with error and there's bail rule, // it then does not continue validating for left rules. if hasBailRule { goto CheckDone } } } index++ } CheckDone: if len(ruleErrorMap) > 0 { return newValidationError( gcode.CodeValidationFailed, []fieldRule{{Name: in.Name, Rule: in.Rule}}, map[string]map[string]error{ in.Name: ruleErrorMap, }, ) } return nil } type doCheckValueRecursivelyInput struct { Value interface{} // Value to be validated. Type reflect.Type // Struct/map/slice type which to be recursively validated. Kind reflect.Kind // Struct/map/slice kind to be asserted in following switch case. ErrorMaps map[string]map[string]error // The validated failed error map. ResultSequenceRules *[]fieldRule // The validated failed rule in sequence. } func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) { switch in.Kind { case reflect.Ptr: v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: in.Value, Type: in.Type.Elem(), Kind: in.Type.Elem().Kind(), ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) case reflect.Struct: // Ignore data, assoc, rules and messages from parent. var ( validator = v.Clone() toBeValidatedObject interface{} ) if in.Type.Kind() == reflect.Ptr { toBeValidatedObject = reflect.New(in.Type.Elem()).Interface() } else { toBeValidatedObject = reflect.New(in.Type).Interface() } validator.assoc = nil validator.rules = nil validator.messages = nil if err := validator.Data(toBeValidatedObject).Assoc(in.Value).Run(ctx); err != nil { // It merges the errors into single error map. for k, m := range err.(*validationError).errors { in.ErrorMaps[k] = m } if in.ResultSequenceRules != nil { *in.ResultSequenceRules = append(*in.ResultSequenceRules, err.(*validationError).rules...) } } case reflect.Map: var ( dataMap = gconv.Map(in.Value) mapTypeElem = in.Type.Elem() mapTypeKind = mapTypeElem.Kind() ) for _, item := range dataMap { v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, Type: mapTypeElem, Kind: mapTypeKind, ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) // Bail feature. if v.bail && len(in.ErrorMaps) > 0 { break } } case reflect.Slice, reflect.Array: var array []interface{} if gjson.Valid(in.Value) { array = gconv.Interfaces(gconv.Bytes(in.Value)) } else { array = gconv.Interfaces(in.Value) } if len(array) == 0 { return } for _, item := range array { v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ Value: item, Type: in.Type.Elem(), Kind: in.Type.Elem().Kind(), ErrorMaps: in.ErrorMaps, ResultSequenceRules: in.ResultSequenceRules, }) // Bail feature. if v.bail && len(in.ErrorMaps) > 0 { break } } } }