github.com/gogf/gf/v2@v2.7.4/util/gvalid/gvalid_validator_check_struct.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/gogf/gf.
     6  
     7  package gvalid
     8  
     9  import (
    10  	"context"
    11  	"reflect"
    12  	"strings"
    13  
    14  	"github.com/gogf/gf/v2/errors/gcode"
    15  	"github.com/gogf/gf/v2/internal/empty"
    16  	"github.com/gogf/gf/v2/os/gstructs"
    17  	"github.com/gogf/gf/v2/util/gconv"
    18  	"github.com/gogf/gf/v2/util/gmeta"
    19  	"github.com/gogf/gf/v2/util/gutil"
    20  )
    21  
    22  func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error {
    23  	var (
    24  		errorMaps           = make(map[string]map[string]error) // Returning error.
    25  		fieldToAliasNameMap = make(map[string]string)           // Field names to alias name map.
    26  		resultSequenceRules = make([]fieldRule, 0)
    27  		isEmptyData         = empty.IsEmpty(v.data)
    28  		isEmptyAssoc        = empty.IsEmpty(v.assoc)
    29  	)
    30  	fieldMap, err := gstructs.FieldMap(gstructs.FieldMapInput{
    31  		Pointer:          object,
    32  		PriorityTagArray: aliasNameTagPriority,
    33  		RecursiveOption:  gstructs.RecursiveOptionEmbedded,
    34  	})
    35  	if err != nil {
    36  		return newValidationErrorByStr(internalObjectErrRuleName, err)
    37  	}
    38  
    39  	// It here must use gstructs.TagFields not gstructs.FieldMap to ensure error sequence.
    40  	tagFields, err := gstructs.TagFields(object, structTagPriority)
    41  	if err != nil {
    42  		return newValidationErrorByStr(internalObjectErrRuleName, err)
    43  	}
    44  	// If there's no struct tag and validation rules, it does nothing and returns quickly.
    45  	if len(tagFields) == 0 && v.messages == nil && isEmptyData && isEmptyAssoc {
    46  		return nil
    47  	}
    48  
    49  	var (
    50  		inputParamMap  map[string]interface{}
    51  		checkRules     = make([]fieldRule, 0)
    52  		nameToRuleMap  = make(map[string]string) // just for internally searching index purpose.
    53  		customMessage  = make(CustomMsg)         // Custom rule error message map.
    54  		checkValueData = v.assoc                 // Ready to be validated data.
    55  	)
    56  	if checkValueData == nil {
    57  		checkValueData = object
    58  	}
    59  	switch assertValue := v.rules.(type) {
    60  	// Sequence tag: []sequence tag
    61  	// Sequence has order for error results.
    62  	case []string:
    63  		for _, tag := range assertValue {
    64  			name, rule, msg := ParseTagValue(tag)
    65  			if len(name) == 0 {
    66  				continue
    67  			}
    68  			if len(msg) > 0 {
    69  				var (
    70  					msgArray  = strings.Split(msg, "|")
    71  					ruleArray = strings.Split(rule, "|")
    72  				)
    73  				for k, ruleKey := range ruleArray {
    74  					// If length of custom messages is lesser than length of rules,
    75  					// the rest rules use the default error messages.
    76  					if len(msgArray) <= k {
    77  						continue
    78  					}
    79  					if len(msgArray[k]) == 0 {
    80  						continue
    81  					}
    82  					array := strings.Split(ruleKey, ":")
    83  					if _, ok := customMessage[name]; !ok {
    84  						customMessage[name] = make(map[string]string)
    85  					}
    86  					customMessage[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k])
    87  				}
    88  			}
    89  			nameToRuleMap[name] = rule
    90  			checkRules = append(checkRules, fieldRule{
    91  				Name: name,
    92  				Rule: rule,
    93  			})
    94  		}
    95  
    96  	// Map type rules does not support sequence.
    97  	// Format: map[key]rule
    98  	case map[string]string:
    99  		nameToRuleMap = assertValue
   100  		for name, rule := range assertValue {
   101  			checkRules = append(checkRules, fieldRule{
   102  				Name: name,
   103  				Rule: rule,
   104  			})
   105  		}
   106  	}
   107  	// If there's no struct tag and validation rules, it does nothing and returns quickly.
   108  	if len(tagFields) == 0 && len(checkRules) == 0 && isEmptyData && isEmptyAssoc {
   109  		return nil
   110  	}
   111  	// Input parameter map handling.
   112  	if v.assoc == nil || !v.useAssocInsteadOfObjectAttributes {
   113  		inputParamMap = make(map[string]interface{})
   114  	} else {
   115  		inputParamMap = gconv.Map(v.assoc)
   116  	}
   117  	// Checks and extends the parameters map with struct alias tag.
   118  	if !v.useAssocInsteadOfObjectAttributes {
   119  		for nameOrTag, field := range fieldMap {
   120  			inputParamMap[nameOrTag] = field.Value.Interface()
   121  			if nameOrTag != field.Name() {
   122  				inputParamMap[field.Name()] = field.Value.Interface()
   123  			}
   124  		}
   125  	}
   126  
   127  	// Merge the custom validation rules with rules in struct tag.
   128  	// The custom rules has the most high priority that can overwrite the struct tag rules.
   129  	for _, field := range tagFields {
   130  		var (
   131  			isMeta          bool
   132  			fieldName       = field.Name()                  // Attribute name.
   133  			name, rule, msg = ParseTagValue(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only.
   134  		)
   135  		if len(name) == 0 {
   136  			if value, ok := fieldToAliasNameMap[fieldName]; ok {
   137  				// It uses alias name of the attribute if its alias name tag exists.
   138  				name = value
   139  			} else {
   140  				// It or else uses the attribute name directly.
   141  				name = fieldName
   142  			}
   143  		} else {
   144  			// It uses the alias name from validation rule.
   145  			fieldToAliasNameMap[fieldName] = name
   146  		}
   147  		// It here extends the params map using alias names.
   148  		// Note that the variable `name` might be alias name or attribute name.
   149  		if _, ok := inputParamMap[name]; !ok {
   150  			if !v.useAssocInsteadOfObjectAttributes {
   151  				inputParamMap[name] = field.Value.Interface()
   152  			} else {
   153  				if name != fieldName {
   154  					if foundKey, foundValue := gutil.MapPossibleItemByKey(inputParamMap, fieldName); foundKey != "" {
   155  						inputParamMap[name] = foundValue
   156  					}
   157  				}
   158  			}
   159  		}
   160  
   161  		if _, ok := nameToRuleMap[name]; !ok {
   162  			if _, ok = nameToRuleMap[fieldName]; ok {
   163  				// If there's alias name,
   164  				// use alias name as its key and remove the field name key.
   165  				nameToRuleMap[name] = nameToRuleMap[fieldName]
   166  				delete(nameToRuleMap, fieldName)
   167  				for index, checkRuleItem := range checkRules {
   168  					if fieldName == checkRuleItem.Name {
   169  						checkRuleItem.Name = name
   170  						checkRules[index] = checkRuleItem
   171  						break
   172  					}
   173  				}
   174  			} else {
   175  				nameToRuleMap[name] = rule
   176  				if fieldValue := field.Value.Interface(); fieldValue != nil {
   177  					_, isMeta = fieldValue.(gmeta.Meta)
   178  				}
   179  				checkRules = append(checkRules, fieldRule{
   180  					Name:      name,
   181  					Rule:      rule,
   182  					IsMeta:    isMeta,
   183  					FieldKind: field.OriginalKind(),
   184  					FieldType: field.Type(),
   185  				})
   186  			}
   187  		} else {
   188  			// The input rules can overwrite the rules in struct tag.
   189  			continue
   190  		}
   191  
   192  		if len(msg) > 0 {
   193  			var (
   194  				msgArray  = strings.Split(msg, "|")
   195  				ruleArray = strings.Split(rule, "|")
   196  			)
   197  			for k, ruleKey := range ruleArray {
   198  				// If length of custom messages is lesser than length of rules,
   199  				// the rest rules use the default error messages.
   200  				if len(msgArray) <= k {
   201  					continue
   202  				}
   203  				if len(msgArray[k]) == 0 {
   204  					continue
   205  				}
   206  				array := strings.Split(ruleKey, ":")
   207  				if _, ok := customMessage[name]; !ok {
   208  					customMessage[name] = make(map[string]string)
   209  				}
   210  				customMessage[name].(map[string]string)[strings.TrimSpace(array[0])] = strings.TrimSpace(msgArray[k])
   211  			}
   212  		}
   213  	}
   214  
   215  	// Custom error messages,
   216  	// which have the most priority than `rules` and struct tag.
   217  	if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 {
   218  		for k, msgName := range msg {
   219  			if aliasName, ok := fieldToAliasNameMap[k]; ok {
   220  				// Overwrite the key of field name.
   221  				customMessage[aliasName] = msgName
   222  			} else {
   223  				customMessage[k] = msgName
   224  			}
   225  		}
   226  	}
   227  
   228  	// Temporary variable for value.
   229  	var value interface{}
   230  
   231  	// It checks the struct recursively if its attribute is a struct/struct slice.
   232  	for _, field := range fieldMap {
   233  		// No validation interface implements check.
   234  		if _, ok := field.Value.Interface().(iNoValidation); ok {
   235  			continue
   236  		}
   237  		// No validation field tag check.
   238  		if _, ok := field.TagLookup(noValidationTagName); ok {
   239  			continue
   240  		}
   241  		if field.IsEmbedded() {
   242  			// The attributes of embedded struct are considered as direct attributes of its parent struct.
   243  			if err = v.doCheckStruct(ctx, field.Value); err != nil {
   244  				// It merges the errors into single error map.
   245  				for k, m := range err.(*validationError).errors {
   246  					errorMaps[k] = m
   247  				}
   248  			}
   249  		} else {
   250  			// The `field.TagValue` is the alias name of field.Name().
   251  			// Eg, value from struct tag `p`.
   252  			if field.TagValue != "" {
   253  				fieldToAliasNameMap[field.Name()] = field.TagValue
   254  			}
   255  			switch field.OriginalKind() {
   256  			case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array:
   257  				// Recursively check attribute slice/map.
   258  				value = getPossibleValueFromMap(
   259  					inputParamMap, field.Name(), fieldToAliasNameMap[field.Name()],
   260  				)
   261  				if empty.IsNil(value) {
   262  					switch field.Kind() {
   263  					case reflect.Map, reflect.Ptr, reflect.Slice, reflect.Array:
   264  						// Nothing to do.
   265  						continue
   266  					default:
   267  					}
   268  				}
   269  				v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
   270  					Value:               value,
   271  					Kind:                field.OriginalKind(),
   272  					Type:                field.Type().Type,
   273  					ErrorMaps:           errorMaps,
   274  					ResultSequenceRules: &resultSequenceRules,
   275  				})
   276  			default:
   277  			}
   278  		}
   279  		if v.bail && len(errorMaps) > 0 {
   280  			break
   281  		}
   282  	}
   283  	if v.bail && len(errorMaps) > 0 {
   284  		return newValidationError(gcode.CodeValidationFailed, resultSequenceRules, errorMaps)
   285  	}
   286  
   287  	// The following logic is the same as some of CheckMap but with sequence support.
   288  	for _, checkRuleItem := range checkRules {
   289  		// it ignores Meta object.
   290  		if !checkRuleItem.IsMeta {
   291  			value = getPossibleValueFromMap(
   292  				inputParamMap, checkRuleItem.Name, fieldToAliasNameMap[checkRuleItem.Name],
   293  			)
   294  		}
   295  		// Empty json string checks according to mapping field kind.
   296  		if value != nil {
   297  			switch checkRuleItem.FieldKind {
   298  			case reflect.Struct, reflect.Map:
   299  				// empty struct or map.
   300  				if gconv.String(value) == emptyJsonObjectStr {
   301  					value = nil
   302  				}
   303  			case reflect.Slice, reflect.Array:
   304  				// empty slice.
   305  				if gconv.String(value) == emptyJsonArrayStr {
   306  					value = []any{}
   307  				}
   308  			default:
   309  			}
   310  		}
   311  		// It checks each rule and its value in loop.
   312  		if validatedError := v.doCheckValue(ctx, doCheckValueInput{
   313  			Name:      checkRuleItem.Name,
   314  			Value:     value,
   315  			ValueType: checkRuleItem.FieldType,
   316  			Rule:      checkRuleItem.Rule,
   317  			Messages:  customMessage[checkRuleItem.Name],
   318  			DataRaw:   checkValueData,
   319  			DataMap:   inputParamMap,
   320  		}); validatedError != nil {
   321  			_, errorItem := validatedError.FirstItem()
   322  			// ============================================================
   323  			// Only in map and struct validations:
   324  			// If value is nil or empty string and has no required* rules,
   325  			// it clears the error message.
   326  			// ============================================================
   327  			if !checkRuleItem.IsMeta && (value == nil || gconv.String(value) == "") {
   328  				required := false
   329  				// rule => error
   330  				for ruleKey := range errorItem {
   331  					// it checks whether current rule is kind of required rule.
   332  					if required = v.checkRuleRequired(ruleKey); required {
   333  						break
   334  					}
   335  				}
   336  				if !required {
   337  					continue
   338  				}
   339  			}
   340  			if _, ok := errorMaps[checkRuleItem.Name]; !ok {
   341  				errorMaps[checkRuleItem.Name] = make(map[string]error)
   342  			}
   343  			for ruleKey, errorItemMsgMap := range errorItem {
   344  				errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap
   345  			}
   346  			// Bail feature.
   347  			if v.bail {
   348  				break
   349  			}
   350  		}
   351  	}
   352  	if len(errorMaps) > 0 {
   353  		return newValidationError(
   354  			gcode.CodeValidationFailed,
   355  			append(checkRules, resultSequenceRules...),
   356  			errorMaps,
   357  		)
   358  	}
   359  	return nil
   360  }
   361  
   362  func getPossibleValueFromMap(inputParamMap map[string]interface{}, fieldName, aliasName string) (value interface{}) {
   363  	_, value = gutil.MapPossibleItemByKey(inputParamMap, fieldName)
   364  	if value == nil && aliasName != "" {
   365  		_, value = gutil.MapPossibleItemByKey(inputParamMap, aliasName)
   366  	}
   367  	return
   368  }