github.com/wangyougui/gf/v2@v2.6.5/util/gvalid/gvalid_validator_check_value.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/wangyougui/gf.
     6  
     7  package gvalid
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"reflect"
    13  	"strings"
    14  
    15  	"github.com/wangyougui/gf/v2/container/gvar"
    16  	"github.com/wangyougui/gf/v2/encoding/gjson"
    17  	"github.com/wangyougui/gf/v2/errors/gcode"
    18  	"github.com/wangyougui/gf/v2/errors/gerror"
    19  	"github.com/wangyougui/gf/v2/text/gregex"
    20  	"github.com/wangyougui/gf/v2/text/gstr"
    21  	"github.com/wangyougui/gf/v2/util/gconv"
    22  	"github.com/wangyougui/gf/v2/util/gvalid/internal/builtin"
    23  )
    24  
    25  type doCheckValueInput struct {
    26  	Name      string                 // Name specifies the name of parameter `value`, which might be the custom tag name of the parameter.
    27  	Value     interface{}            // Value specifies the value for the rules to be validated.
    28  	ValueType reflect.Type           // ValueType specifies the type of the value, mainly used for value type id retrieving.
    29  	Rule      string                 // Rule specifies the validation rules string, like "required", "required|between:1,100", etc.
    30  	Messages  interface{}            // Messages specifies the custom error messages for this rule from parameters input, which is usually type of map/slice.
    31  	DataRaw   interface{}            // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value.
    32  	DataMap   map[string]interface{} // DataMap specifies the map that is converted from `dataRaw`. It is usually used internally
    33  }
    34  
    35  // doCheckValue does the really rules validation for single key-value.
    36  func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Error {
    37  	// If there's no validation rules, it does nothing and returns quickly.
    38  	if in.Rule == "" {
    39  		return nil
    40  	}
    41  	// It converts value to string and then does the validation.
    42  	var (
    43  		// Do not trim it as the space is also part of the value.
    44  		ruleErrorMap = make(map[string]error)
    45  	)
    46  	// Custom error messages handling.
    47  	var (
    48  		msgArray     = make([]string, 0)
    49  		customMsgMap = make(map[string]string)
    50  	)
    51  	switch messages := in.Messages.(type) {
    52  	case string:
    53  		msgArray = strings.Split(messages, "|")
    54  
    55  	default:
    56  		for k, message := range gconv.Map(in.Messages) {
    57  			customMsgMap[k] = gconv.String(message)
    58  		}
    59  	}
    60  	// Handle the char '|' in the rule,
    61  	// which makes this rule separated into multiple rules.
    62  	ruleItems := strings.Split(strings.TrimSpace(in.Rule), "|")
    63  	for i := 0; ; {
    64  		array := strings.Split(ruleItems[i], ":")
    65  		if builtin.GetRule(array[0]) == nil && v.getCustomRuleFunc(array[0]) == nil {
    66  			// ============================ SPECIAL ============================
    67  			// Special `regex` and `not-regex` rules.
    68  			// Merge the regex pattern if there are special chars, like ':', '|', in pattern.
    69  			// ============================ SPECIAL ============================
    70  			var (
    71  				ruleNameRegexLengthMatch    bool
    72  				ruleNameNotRegexLengthMatch bool
    73  			)
    74  			if i > 0 {
    75  				ruleItem := ruleItems[i-1]
    76  				if len(ruleItem) >= len(ruleNameRegex) && ruleItem[:len(ruleNameRegex)] == ruleNameRegex {
    77  					ruleNameRegexLengthMatch = true
    78  				}
    79  				if len(ruleItem) >= len(ruleNameNotRegex) && ruleItem[:len(ruleNameNotRegex)] == ruleNameNotRegex {
    80  					ruleNameNotRegexLengthMatch = true
    81  				}
    82  			}
    83  			if i > 0 && (ruleNameRegexLengthMatch || ruleNameNotRegexLengthMatch) {
    84  				ruleItems[i-1] += "|" + ruleItems[i]
    85  				ruleItems = append(ruleItems[:i], ruleItems[i+1:]...)
    86  			} else {
    87  				return newValidationErrorByStr(
    88  					internalRulesErrRuleName,
    89  					errors.New(internalRulesErrRuleName+": "+ruleItems[i]),
    90  				)
    91  			}
    92  		} else {
    93  			i++
    94  		}
    95  		if i == len(ruleItems) {
    96  			break
    97  		}
    98  	}
    99  	var (
   100  		hasBailRule        = v.bail
   101  		hasForeachRule     = v.foreach
   102  		hasCaseInsensitive = v.caseInsensitive
   103  	)
   104  	for index := 0; index < len(ruleItems); {
   105  		var (
   106  			err         error
   107  			results     = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule.
   108  			ruleKey     = gstr.Trim(results[1])                          // rule key like "max" in rule "max: 6"
   109  			rulePattern = gstr.Trim(results[2])                          // rule pattern is like "6" in rule:"max:6"
   110  		)
   111  
   112  		if !hasBailRule && ruleKey == ruleNameBail {
   113  			hasBailRule = true
   114  		}
   115  		if !hasForeachRule && ruleKey == ruleNameForeach {
   116  			hasForeachRule = true
   117  		}
   118  		if !hasCaseInsensitive && ruleKey == ruleNameCi {
   119  			hasCaseInsensitive = true
   120  		}
   121  
   122  		// Ignore logic executing for marked rules.
   123  		if decorativeRuleMap[ruleKey] {
   124  			index++
   125  			continue
   126  		}
   127  
   128  		if len(msgArray) > index {
   129  			customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index])
   130  		}
   131  
   132  		var (
   133  			message        = v.getErrorMessageByRule(ctx, ruleKey, customMsgMap)
   134  			customRuleFunc = v.getCustomRuleFunc(ruleKey)
   135  			builtinRule    = builtin.GetRule(ruleKey)
   136  			foreachValues  = []interface{}{in.Value}
   137  		)
   138  		if hasForeachRule {
   139  			// As it marks `foreach`, so it converts the value to slice.
   140  			foreachValues = gconv.Interfaces(in.Value)
   141  			// Reset `foreach` rule as it only takes effect just once for next rule.
   142  			hasForeachRule = false
   143  		}
   144  
   145  		for _, value := range foreachValues {
   146  			switch {
   147  			// Custom validation rules.
   148  			case customRuleFunc != nil:
   149  				err = customRuleFunc(ctx, RuleFuncInput{
   150  					Rule:      ruleItems[index],
   151  					Message:   message,
   152  					Field:     in.Name,
   153  					ValueType: in.ValueType,
   154  					Value:     gvar.New(value),
   155  					Data:      gvar.New(in.DataRaw),
   156  				})
   157  
   158  			// Builtin validation rules.
   159  			case customRuleFunc == nil && builtinRule != nil:
   160  				err = builtinRule.Run(builtin.RunInput{
   161  					RuleKey:     ruleKey,
   162  					RulePattern: rulePattern,
   163  					Field:       in.Name,
   164  					ValueType:   in.ValueType,
   165  					Value:       gvar.New(value),
   166  					Data:        gvar.New(in.DataRaw),
   167  					Message:     message,
   168  					Option: builtin.RunOption{
   169  						CaseInsensitive: hasCaseInsensitive,
   170  					},
   171  				})
   172  
   173  			default:
   174  				// It never comes across here.
   175  			}
   176  
   177  			// Error handling.
   178  			if err != nil {
   179  				// Error variable replacement for error message.
   180  				if errMsg := err.Error(); gstr.Contains(errMsg, "{") {
   181  					errMsg = gstr.ReplaceByMap(errMsg, map[string]string{
   182  						"{field}":     in.Name,             // Field name of the `value`.
   183  						"{value}":     gconv.String(value), // Current validating value.
   184  						"{pattern}":   rulePattern,         // The variable part of the rule.
   185  						"{attribute}": in.Name,             // The same as `{field}`. It is deprecated.
   186  					})
   187  					errMsg, _ = gregex.ReplaceString(`\s{2,}`, ` `, errMsg)
   188  					err = errors.New(errMsg)
   189  				}
   190  				// The error should have stack info to indicate the error position.
   191  				if !gerror.HasStack(err) {
   192  					err = gerror.NewCode(gcode.CodeValidationFailed, err.Error())
   193  				}
   194  				// The error should have error code that is `gcode.CodeValidationFailed`.
   195  				if gerror.Code(err) == gcode.CodeNil {
   196  					// TODO it's better using interface?
   197  					if e, ok := err.(*gerror.Error); ok {
   198  						e.SetCode(gcode.CodeValidationFailed)
   199  					}
   200  				}
   201  				ruleErrorMap[ruleKey] = err
   202  
   203  				// If it is with error and there's bail rule,
   204  				// it then does not continue validating for left rules.
   205  				if hasBailRule {
   206  					goto CheckDone
   207  				}
   208  			}
   209  		}
   210  		index++
   211  	}
   212  
   213  CheckDone:
   214  	if len(ruleErrorMap) > 0 {
   215  		return newValidationError(
   216  			gcode.CodeValidationFailed,
   217  			[]fieldRule{{Name: in.Name, Rule: in.Rule}},
   218  			map[string]map[string]error{
   219  				in.Name: ruleErrorMap,
   220  			},
   221  		)
   222  	}
   223  	return nil
   224  }
   225  
   226  type doCheckValueRecursivelyInput struct {
   227  	Value               interface{}                 // Value to be validated.
   228  	Type                reflect.Type                // Struct/map/slice type which to be recursively validated.
   229  	Kind                reflect.Kind                // Struct/map/slice kind to be asserted in following switch case.
   230  	ErrorMaps           map[string]map[string]error // The validated failed error map.
   231  	ResultSequenceRules *[]fieldRule                // The validated failed rule in sequence.
   232  }
   233  
   234  func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) {
   235  	switch in.Kind {
   236  	case reflect.Ptr:
   237  		v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
   238  			Value:               in.Value,
   239  			Type:                in.Type.Elem(),
   240  			Kind:                in.Type.Elem().Kind(),
   241  			ErrorMaps:           in.ErrorMaps,
   242  			ResultSequenceRules: in.ResultSequenceRules,
   243  		})
   244  
   245  	case reflect.Struct:
   246  		// Ignore data, assoc, rules and messages from parent.
   247  		var (
   248  			validator           = v.Clone()
   249  			toBeValidatedObject interface{}
   250  		)
   251  		if in.Type.Kind() == reflect.Ptr {
   252  			toBeValidatedObject = reflect.New(in.Type.Elem()).Interface()
   253  		} else {
   254  			toBeValidatedObject = reflect.New(in.Type).Interface()
   255  		}
   256  		validator.assoc = nil
   257  		validator.rules = nil
   258  		validator.messages = nil
   259  		if err := validator.Data(toBeValidatedObject).Assoc(in.Value).Run(ctx); err != nil {
   260  			// It merges the errors into single error map.
   261  			for k, m := range err.(*validationError).errors {
   262  				in.ErrorMaps[k] = m
   263  			}
   264  			if in.ResultSequenceRules != nil {
   265  				*in.ResultSequenceRules = append(*in.ResultSequenceRules, err.(*validationError).rules...)
   266  			}
   267  		}
   268  
   269  	case reflect.Map:
   270  		var (
   271  			dataMap     = gconv.Map(in.Value)
   272  			mapTypeElem = in.Type.Elem()
   273  			mapTypeKind = mapTypeElem.Kind()
   274  		)
   275  		for _, item := range dataMap {
   276  			v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
   277  				Value:               item,
   278  				Type:                mapTypeElem,
   279  				Kind:                mapTypeKind,
   280  				ErrorMaps:           in.ErrorMaps,
   281  				ResultSequenceRules: in.ResultSequenceRules,
   282  			})
   283  			// Bail feature.
   284  			if v.bail && len(in.ErrorMaps) > 0 {
   285  				break
   286  			}
   287  		}
   288  
   289  	case reflect.Slice, reflect.Array:
   290  		var array []interface{}
   291  		if gjson.Valid(in.Value) {
   292  			array = gconv.Interfaces(gconv.Bytes(in.Value))
   293  		} else {
   294  			array = gconv.Interfaces(in.Value)
   295  		}
   296  		if len(array) == 0 {
   297  			return
   298  		}
   299  		for _, item := range array {
   300  			v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
   301  				Value:               item,
   302  				Type:                in.Type.Elem(),
   303  				Kind:                in.Type.Elem().Kind(),
   304  				ErrorMaps:           in.ErrorMaps,
   305  				ResultSequenceRules: in.ResultSequenceRules,
   306  			})
   307  			// Bail feature.
   308  			if v.bail && len(in.ErrorMaps) > 0 {
   309  				break
   310  			}
   311  		}
   312  	}
   313  }