dev.azure.com/aidainnovazione0090/DeviceManager/_git/go-mod-core-contracts@v1.0.2/common/validator.go (about)

     1  //go:build !no_dto_validator
     2  
     3  //
     4  // Copyright (C) 2020-2021 IOTech Ltd
     5  //
     6  // SPDX-License-Identifier: Apache-2.0
     7  
     8  package common
     9  
    10  import (
    11  	"fmt"
    12  	"reflect"
    13  	"regexp"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/go-playground/validator/v10"
    18  	"github.com/google/uuid"
    19  
    20  	"dev.azure.com/aidainnovazione0090/DeviceManager/_git/go-mod-core-contracts/errors"
    21  )
    22  
    23  var val *validator.Validate
    24  
    25  const (
    26  	dtoDurationTag                     = "edgex-dto-duration"
    27  	dtoUuidTag                         = "edgex-dto-uuid"
    28  	dtoNoneEmptyStringTag              = "edgex-dto-none-empty-string"
    29  	dtoValueType                       = "edgex-dto-value-type"
    30  	dtoRFC3986UnreservedCharTag        = "edgex-dto-rfc3986-unreserved-chars"
    31  	emptyOrDtoRFC3986UnreservedCharTag = "len=0|" + dtoRFC3986UnreservedCharTag
    32  	dtoInterDatetimeTag                = "edgex-dto-interval-datetime"
    33  	dtoAlphaNumericWithSymbols         = "edgex-dto-alphanumeric-with-symbols"
    34  )
    35  
    36  const (
    37  	// Per https://tools.ietf.org/html/rfc3986#section-2.3, unreserved characters= ALPHA / DIGIT / "-" / "." / "_" / "~"
    38  	// Also due to names used in topics for Redis Pub/Sub, "."are not allowed
    39  	rFC3986UnreservedCharsRegexString = "^[a-zA-Z0-9-_~:;=]+$"
    40  	intervalDatetimeLayout            = "20060102T150405"
    41  	name                              = "Name"
    42  	alphaNumericWithSymbols           = "^[a-zA-Z0-9-_~:;=+ ]+$"
    43  )
    44  
    45  var (
    46  	rFC3986UnreservedCharsRegex  = regexp.MustCompile(rFC3986UnreservedCharsRegexString)
    47  	alphaNumericWithSymbolsRegex = regexp.MustCompile(alphaNumericWithSymbols)
    48  )
    49  
    50  func init() {
    51  	val = validator.New()
    52  	_ = val.RegisterValidation(dtoDurationTag, ValidateDuration)
    53  	_ = val.RegisterValidation(dtoUuidTag, ValidateDtoUuid)
    54  	_ = val.RegisterValidation(dtoNoneEmptyStringTag, ValidateDtoNoneEmptyString)
    55  	_ = val.RegisterValidation(dtoValueType, ValidateValueType)
    56  	_ = val.RegisterValidation(dtoRFC3986UnreservedCharTag, ValidateDtoRFC3986UnreservedChars)
    57  	_ = val.RegisterValidation(dtoInterDatetimeTag, ValidateIntervalDatetime)
    58  	_ = val.RegisterValidation(dtoAlphaNumericWithSymbols, ValidateDtoAlphaNumericWithSymbols)
    59  }
    60  
    61  // Validate function will use the validator package to validate the struct annotation
    62  func Validate(a interface{}) error {
    63  	err := val.Struct(a)
    64  	// translate all error at once
    65  	if err != nil {
    66  		errs := err.(validator.ValidationErrors)
    67  		var errMsg []string
    68  		for _, e := range errs {
    69  			errMsg = append(errMsg, getErrorMessage(e))
    70  		}
    71  		return errors.NewCommonEdgeX(errors.KindContractInvalid, strings.Join(errMsg, "; "), nil)
    72  	}
    73  	return nil
    74  }
    75  
    76  // Internal: generate representative validation error messages
    77  func getErrorMessage(e validator.FieldError) string {
    78  	tag := e.Tag()
    79  	// StructNamespace returns the namespace for the field error, with the field's actual name.
    80  	fieldName := e.StructNamespace()
    81  	fieldValue := e.Param()
    82  	var msg string
    83  	switch tag {
    84  	case "uuid":
    85  		msg = fmt.Sprintf("%s field needs a uuid", fieldName)
    86  	case "required":
    87  		msg = fmt.Sprintf("%s field is required", fieldName)
    88  	case "required_without":
    89  		msg = fmt.Sprintf("%s field is required if the %s is not present", fieldName, fieldValue)
    90  	case "len":
    91  		msg = fmt.Sprintf("The length of %s field is not %s", fieldName, fieldValue)
    92  	case "oneof":
    93  		msg = fmt.Sprintf("%s field should be one of %s", fieldName, fieldValue)
    94  	case "gt":
    95  		msg = fmt.Sprintf("%s field should greater than %s", fieldName, fieldValue)
    96  	case dtoDurationTag:
    97  		msg = fmt.Sprintf("%s field should follows the ISO 8601 Durations format. Eg,100ms, 24h", fieldName)
    98  	case dtoUuidTag:
    99  		msg = fmt.Sprintf("%s field needs a uuid", fieldName)
   100  	case dtoNoneEmptyStringTag:
   101  		msg = fmt.Sprintf("%s field should not be empty string", fieldName)
   102  	case dtoRFC3986UnreservedCharTag, emptyOrDtoRFC3986UnreservedCharTag:
   103  		msg = fmt.Sprintf("%s field only allows unreserved characters which are ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_~:;=", fieldName)
   104  	default:
   105  		msg = fmt.Sprintf("%s field validation failed on the %s tag", fieldName, tag)
   106  	}
   107  	return msg
   108  }
   109  
   110  // ValidateDuration validate field which should follow the ISO 8601 Durations format
   111  func ValidateDuration(fl validator.FieldLevel) bool {
   112  	_, err := time.ParseDuration(fl.Field().String())
   113  	return err == nil
   114  }
   115  
   116  // ValidateDtoUuid used to check the UpdateDTO uuid pointer value
   117  // Currently, required_without can not correct work with other tag, so write custom tag instead.
   118  // Issue can refer to https://github.com/go-playground/validator/issues/624
   119  func ValidateDtoUuid(fl validator.FieldLevel) bool {
   120  	idField := fl.Field()
   121  	// Skip the validation if the pointer value is nil
   122  	if isNilPointer(idField) {
   123  		return true
   124  	}
   125  
   126  	// The Id field should accept the empty string if the Name field is provided
   127  	nameField := fl.Parent().FieldByName(name)
   128  	if len(strings.TrimSpace(idField.String())) == 0 && !isNilPointer(nameField) && len(nameField.Elem().String()) > 0 {
   129  		return true
   130  	}
   131  
   132  	_, err := uuid.Parse(idField.String())
   133  	return err == nil
   134  }
   135  
   136  // ValidateDtoNoneEmptyString used to check the UpdateDTO name pointer value
   137  func ValidateDtoNoneEmptyString(fl validator.FieldLevel) bool {
   138  	val := fl.Field()
   139  	// Skip the validation if the pointer value is nil
   140  	if isNilPointer(val) {
   141  		return true
   142  	}
   143  	// The string value should not be empty
   144  	if len(strings.TrimSpace(val.String())) > 0 {
   145  		return true
   146  	} else {
   147  		return false
   148  	}
   149  }
   150  
   151  // ValidateValueType checks whether the valueType is valid
   152  func ValidateValueType(fl validator.FieldLevel) bool {
   153  	valueType := fl.Field().String()
   154  	for _, v := range valueTypes {
   155  		if strings.EqualFold(valueType, v) {
   156  			return true
   157  		}
   158  	}
   159  	return false
   160  }
   161  
   162  // ValidateDtoRFC3986UnreservedChars used to check if DTO's name pointer value only contains unreserved characters as
   163  // defined in https://tools.ietf.org/html/rfc3986#section-2.3
   164  func ValidateDtoRFC3986UnreservedChars(fl validator.FieldLevel) bool {
   165  	val := fl.Field()
   166  	// Skip the validation if the pointer value is nil
   167  	if isNilPointer(val) {
   168  		return true
   169  	} else {
   170  		return rFC3986UnreservedCharsRegex.MatchString(val.String())
   171  	}
   172  }
   173  
   174  // ValidateIntervalDatetime validate Interval's datetime field which should follow the ISO 8601 format YYYYMMDD'T'HHmmss
   175  func ValidateIntervalDatetime(fl validator.FieldLevel) bool {
   176  	_, err := time.Parse(intervalDatetimeLayout, fl.Field().String())
   177  	return err == nil
   178  }
   179  
   180  func isNilPointer(value reflect.Value) bool {
   181  	return value.Kind() == reflect.Ptr && value.IsNil()
   182  }
   183  
   184  func ValidateDtoAlphaNumericWithSymbols(fl validator.FieldLevel) bool {
   185  	val := fl.Field()
   186  	// Skip the validation if the pointer value is nil
   187  	if isNilPointer(val) {
   188  		return true
   189  	} else {
   190  		return alphaNumericWithSymbolsRegex.MatchString(val.String())
   191  	}
   192  }