github.com/System-Glitch/goyave/v2@v2.10.3-0.20200819142921-51011e75d504/validation/validator.go (about)

     1  package validation
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strings"
     7  
     8  	"github.com/System-Glitch/goyave/v2/lang"
     9  )
    10  
    11  // Ruler adapter interface for method dispatching between RuleSet and Rules
    12  // at route registration time. Allows to input both of these types as parameters
    13  // of the Route.Validate method.
    14  type Ruler interface {
    15  	AsRules() *Rules
    16  }
    17  
    18  // RuleFunc function defining a validation rule.
    19  // Passing rules should return true, false otherwise.
    20  //
    21  // Rules can modifiy the validated value if needed.
    22  // For example, the "numeric" rule converts the data to float64 if it's a string.
    23  type RuleFunc func(string, interface{}, []string, map[string]interface{}) bool
    24  
    25  // RuleDefinition is the definition of a rule, containing the information
    26  // related to the behavior executed on validation-time.
    27  type RuleDefinition struct {
    28  
    29  	// The Function field is the function that will be executed
    30  	Function RuleFunc
    31  
    32  	// The minimum amount of parameters
    33  	RequiredParameters int
    34  
    35  	// A type rule is a rule that checks if a field has a certain type
    36  	// and can convert the raw value to a value fitting. For example, the UUID
    37  	// rule is a type rule because it takes a string as input, checks if it's a
    38  	// valid UUID and converts it to a "uuid.UUID".
    39  	// The "array" rule is an exception. It does convert the value to a new slice of
    40  	// the correct type if provided, but is not considered a type rule to avoid being
    41  	// able to be used as parameter for itself ("array:array").
    42  	IsType bool
    43  
    44  	// Type-dependent rules are rules that can be used with different field types
    45  	// (numeric, string, arrays and files) and have a different validation messages
    46  	// depending on the type.
    47  	// The language entry used will be "validation.rules.rulename.type"
    48  	IsTypeDependent bool
    49  }
    50  
    51  // RuleSet is a request rules definition. Each entry is a field in the request.
    52  type RuleSet map[string][]string
    53  
    54  var _ Ruler = (RuleSet)(nil) // implements Ruler
    55  
    56  // AsRules parses and checks this RuleSet and returns it as Rules.
    57  func (r RuleSet) AsRules() *Rules {
    58  	return r.parse()
    59  }
    60  
    61  // Parse converts the more convenient RuleSet validation rules syntax to
    62  // a Rules map.
    63  func (r RuleSet) parse() *Rules {
    64  	rules := &Rules{
    65  		Fields: make(map[string]*Field, len(r)),
    66  	}
    67  	for k, r := range r {
    68  		field := &Field{
    69  			Rules: make([]*Rule, 0, len(r)),
    70  		}
    71  		for _, v := range r {
    72  			field.Rules = append(field.Rules, parseRule(v))
    73  		}
    74  		rules.Fields[k] = field
    75  	}
    76  	rules.check()
    77  	return rules
    78  }
    79  
    80  // Rule is a component of rule sets for route validation. Each validated fields
    81  // has one or multiple validation rules. The goal of this struct is to
    82  // gather information about how to use a rule definition for this field.
    83  // This inludes the rule name (referring to a RuleDefinition), the parameters
    84  // and the array dimension for array validation.
    85  type Rule struct {
    86  	Name           string
    87  	Params         []string
    88  	ArrayDimension uint8
    89  }
    90  
    91  // Field is a component of route validation. A Field is a value in
    92  // a Rules map, the key being the name of the field.
    93  type Field struct {
    94  	Rules      []*Rule
    95  	isArray    bool
    96  	isRequired bool
    97  	isNullable bool
    98  }
    99  
   100  // IsRequired check if a field has the "required" rule
   101  func (v *Field) IsRequired() bool {
   102  	return v.isRequired
   103  }
   104  
   105  // IsNullable check if a field has the "nullable" rule
   106  func (v *Field) IsNullable() bool {
   107  	return v.isNullable
   108  }
   109  
   110  // IsArray check if a field has the "array" rule
   111  func (v *Field) IsArray() bool {
   112  	return v.isArray
   113  }
   114  
   115  // check if rules meet the minimum parameters requirement and update
   116  // the isRequired, isNullable and isArray fields.
   117  func (v *Field) check() {
   118  	for _, rule := range v.Rules {
   119  		switch rule.Name {
   120  		case "confirmed", "file", "mime", "image", "extension", "count",
   121  			"count_min", "count_max", "count_between":
   122  			if rule.ArrayDimension != 0 {
   123  				panic(fmt.Sprintf("Cannot use rule \"%s\" in array validation", rule.Name))
   124  			}
   125  		case "required":
   126  			v.isRequired = true
   127  		case "nullable":
   128  			v.isNullable = true
   129  			continue
   130  		case "array":
   131  			v.isArray = true
   132  		}
   133  
   134  		def, exists := validationRules[rule.Name]
   135  		if !exists {
   136  			panic(fmt.Sprintf("Rule \"%s\" doesn't exist", rule.Name))
   137  		}
   138  		if len(rule.Params) < def.RequiredParameters {
   139  			panic(fmt.Sprintf("Rule \"%s\" requires %d parameter(s)", rule.Name, def.RequiredParameters))
   140  		}
   141  	}
   142  }
   143  
   144  // FieldMap is an alias to shorten verbose validation rules declaration.
   145  // Maps a field name (key) with a Field struct (value).
   146  type FieldMap map[string]*Field
   147  
   148  // Rules is a component of route validation and maps a
   149  // field name (key) with a Field struct (value).
   150  type Rules struct {
   151  	Fields  map[string]*Field
   152  	checked bool
   153  }
   154  
   155  var _ Ruler = (*Rules)(nil) // implements Ruler
   156  
   157  // AsRules performs the checking and returns the same Rules instance.
   158  func (r *Rules) AsRules() *Rules {
   159  	r.check()
   160  	return r
   161  }
   162  
   163  // check all rules in this set. This function will panic if
   164  // any of the rules doesn't refer to an existing RuleDefinition, doesn't
   165  // meet the parameters requirement, or if the rule cannot be used in array validation
   166  // while ArrayDimension is not equal to 0.
   167  func (r *Rules) check() {
   168  	if !r.checked {
   169  		for _, field := range r.Fields {
   170  			field.check()
   171  		}
   172  		r.checked = true
   173  	}
   174  }
   175  
   176  // Errors is a map of validation errors with the field name as a key.
   177  type Errors map[string][]string
   178  
   179  var validationRules map[string]*RuleDefinition
   180  
   181  func init() {
   182  	validationRules = map[string]*RuleDefinition{
   183  		"required":           {validateRequired, 0, false, false},
   184  		"numeric":            {validateNumeric, 0, true, false},
   185  		"integer":            {validateInteger, 0, true, false},
   186  		"min":                {validateMin, 1, false, true},
   187  		"max":                {validateMax, 1, false, true},
   188  		"between":            {validateBetween, 2, false, true},
   189  		"greater_than":       {validateGreaterThan, 1, false, true},
   190  		"greater_than_equal": {validateGreaterThanEqual, 1, false, true},
   191  		"lower_than":         {validateLowerThan, 1, false, true},
   192  		"lower_than_equal":   {validateLowerThanEqual, 1, false, true},
   193  		"string":             {validateString, 0, true, false},
   194  		"array":              {validateArray, 0, false, false},
   195  		"distinct":           {validateDistinct, 0, false, false},
   196  		"digits":             {validateDigits, 0, false, false},
   197  		"regex":              {validateRegex, 1, false, false},
   198  		"email":              {validateEmail, 0, false, false},
   199  		"size":               {validateSize, 1, false, true},
   200  		"alpha":              {validateAlpha, 0, false, false},
   201  		"alpha_dash":         {validateAlphaDash, 0, false, false},
   202  		"alpha_num":          {validateAlphaNumeric, 0, false, false},
   203  		"starts_with":        {validateStartsWith, 1, false, false},
   204  		"ends_with":          {validateEndsWith, 1, false, false},
   205  		"in":                 {validateIn, 1, false, false},
   206  		"not_in":             {validateNotIn, 1, false, false},
   207  		"in_array":           {validateInArray, 1, false, false},
   208  		"not_in_array":       {validateNotInArray, 1, false, false},
   209  		"timezone":           {validateTimezone, 0, true, false},
   210  		"ip":                 {validateIP, 0, true, false},
   211  		"ipv4":               {validateIPv4, 0, true, false},
   212  		"ipv6":               {validateIPv6, 0, true, false},
   213  		"json":               {validateJSON, 0, true, false},
   214  		"url":                {validateURL, 0, true, false},
   215  		"uuid":               {validateUUID, 0, true, false},
   216  		"bool":               {validateBool, 0, true, false},
   217  		"same":               {validateSame, 1, false, false},
   218  		"different":          {validateDifferent, 1, false, false},
   219  		"confirmed":          {validateConfirmed, 0, false, false},
   220  		"file":               {validateFile, 0, false, false},
   221  		"mime":               {validateMIME, 1, false, false},
   222  		"image":              {validateImage, 0, false, false},
   223  		"extension":          {validateExtension, 1, false, false},
   224  		"count":              {validateCount, 1, false, false},
   225  		"count_min":          {validateCountMin, 1, false, false},
   226  		"count_max":          {validateCountMax, 1, false, false},
   227  		"count_between":      {validateCountBetween, 2, false, false},
   228  		"date":               {validateDate, 0, true, false},
   229  		"before":             {validateBefore, 1, false, false},
   230  		"before_equal":       {validateBeforeEqual, 1, false, false},
   231  		"after":              {validateAfter, 1, false, false},
   232  		"after_equal":        {validateAfterEqual, 1, false, false},
   233  		"date_equals":        {validateDateEquals, 1, false, false},
   234  		"date_between":       {validateDateBetween, 2, false, false},
   235  	}
   236  }
   237  
   238  // AddRule register a validation rule.
   239  // The rule will be usable in request validation by using the
   240  // given rule name.
   241  //
   242  // Type-dependent messages let you define a different message for
   243  // numeric, string, arrays and files.
   244  // The language entry used will be "validation.rules.rulename.type"
   245  func AddRule(name string, rule *RuleDefinition) {
   246  	if _, exists := validationRules[name]; exists {
   247  		panic(fmt.Sprintf("Rule %s already exists", name))
   248  	}
   249  	validationRules[name] = rule
   250  }
   251  
   252  // Validate the given data with the given rule set.
   253  // If all validation rules pass, returns an empty "validation.Errors".
   254  // Third parameter tells the function if the data comes from a JSON request.
   255  // Last parameter sets the language of the validation error messages.
   256  func Validate(data map[string]interface{}, rules Ruler, isJSON bool, language string) Errors {
   257  	if data == nil {
   258  		var malformedMessage string
   259  		if isJSON {
   260  			malformedMessage = lang.Get(language, "malformed-json")
   261  		} else {
   262  			malformedMessage = lang.Get(language, "malformed-request")
   263  		}
   264  		return map[string][]string{"error": {malformedMessage}}
   265  	}
   266  
   267  	return validate(data, isJSON, rules.AsRules(), language)
   268  }
   269  
   270  func validate(data map[string]interface{}, isJSON bool, rules *Rules, language string) Errors {
   271  	errors := Errors{}
   272  
   273  	for fieldName, field := range rules.Fields {
   274  		if !field.IsNullable() && data[fieldName] == nil {
   275  			delete(data, fieldName)
   276  		}
   277  
   278  		if !field.IsRequired() && !validateRequired(fieldName, data[fieldName], nil, data) {
   279  			continue
   280  		}
   281  
   282  		convertArray(isJSON, fieldName, field, data) // Convert single value arrays in url-encoded requests
   283  
   284  		for _, rule := range field.Rules {
   285  			if rule.Name == "nullable" {
   286  				if data[fieldName] == nil {
   287  					break
   288  				}
   289  				continue
   290  			}
   291  
   292  			if rule.ArrayDimension > 0 {
   293  				if ok, errorValue := validateRuleInArray(rule, fieldName, rule.ArrayDimension, data); !ok {
   294  					errors[fieldName] = append(
   295  						errors[fieldName],
   296  						processPlaceholders(fieldName, rule.Name, rule.Params, getMessage(field.Rules, rule, errorValue, language), language),
   297  					)
   298  				}
   299  			} else if !validationRules[rule.Name].Function(fieldName, data[fieldName], rule.Params, data) {
   300  				errors[fieldName] = append(
   301  					errors[fieldName],
   302  					processPlaceholders(fieldName, rule.Name, rule.Params, getMessage(field.Rules, rule, reflect.ValueOf(data[fieldName]), language), language),
   303  				)
   304  			}
   305  		}
   306  	}
   307  	return errors
   308  }
   309  
   310  func validateRuleInArray(rule *Rule, fieldName string, arrayDimension uint8, data map[string]interface{}) (bool, reflect.Value) {
   311  	if t := GetFieldType(data[fieldName]); t != "array" {
   312  		return false, reflect.ValueOf(data[fieldName])
   313  	}
   314  
   315  	converted := false
   316  	var convertedArr reflect.Value
   317  	list := reflect.ValueOf(data[fieldName])
   318  	length := list.Len()
   319  	for i := 0; i < length; i++ {
   320  		v := list.Index(i)
   321  		value := v.Interface()
   322  		tmpData := map[string]interface{}{fieldName: value}
   323  		if arrayDimension > 1 {
   324  			ok, errorValue := validateRuleInArray(rule, fieldName, arrayDimension-1, tmpData)
   325  			if !ok {
   326  				return false, errorValue
   327  			}
   328  		} else if !validationRules[rule.Name].Function(fieldName, value, rule.Params, tmpData) {
   329  			return false, v
   330  		}
   331  
   332  		// Update original array if value has been modified.
   333  		if rule.Name == "array" {
   334  			if !converted { // Ensure field is a two dimensional array of the correct type
   335  				convertedArr = reflect.MakeSlice(reflect.SliceOf(reflect.TypeOf(tmpData[fieldName])), 0, length)
   336  				converted = true
   337  			}
   338  			convertedArr = reflect.Append(convertedArr, reflect.ValueOf(tmpData[fieldName]))
   339  		} else {
   340  			v.Set(reflect.ValueOf(tmpData[fieldName]))
   341  		}
   342  	}
   343  
   344  	if converted {
   345  		data[fieldName] = convertedArr.Interface()
   346  	}
   347  	return true, reflect.Value{}
   348  }
   349  
   350  func convertArray(isJSON bool, fieldName string, field *Field, data map[string]interface{}) {
   351  	if !isJSON {
   352  		val := data[fieldName]
   353  		rv := reflect.ValueOf(val)
   354  		kind := rv.Kind().String()
   355  		if field.IsArray() && kind != "slice" {
   356  			rt := reflect.TypeOf(val)
   357  			slice := reflect.MakeSlice(reflect.SliceOf(rt), 0, 1)
   358  			slice = reflect.Append(slice, rv)
   359  			data[fieldName] = slice.Interface()
   360  		}
   361  	}
   362  }
   363  
   364  func getMessage(rules []*Rule, rule *Rule, value reflect.Value, language string) string {
   365  	langEntry := "validation.rules." + rule.Name
   366  	if validationRules[rule.Name].IsTypeDependent {
   367  		expectedType := findTypeRule(rules, rule.ArrayDimension)
   368  		if expectedType == "unsupported" {
   369  			langEntry += "." + getFieldType(value)
   370  		} else {
   371  			langEntry += "." + expectedType
   372  		}
   373  	}
   374  
   375  	if rule.ArrayDimension > 0 {
   376  		langEntry += ".array"
   377  	}
   378  
   379  	return lang.Get(language, langEntry)
   380  }
   381  
   382  // findTypeRule find the expected type of a field for a given array dimension.
   383  func findTypeRule(rules []*Rule, arrayDimension uint8) string {
   384  	for _, rule := range rules {
   385  		if rule.ArrayDimension == arrayDimension-1 && rule.Name == "array" && len(rule.Params) > 0 {
   386  			return rule.Params[0]
   387  		} else if rule.ArrayDimension == arrayDimension && validationRules[rule.Name].IsType {
   388  			return rule.Name
   389  		}
   390  	}
   391  	return "unsupported"
   392  }
   393  
   394  // GetFieldType returns the non-technical type of the given "value" interface.
   395  // This is used by validation rules to know if the input data is a candidate
   396  // for validation or not and is especially useful for type-dependent rules.
   397  //  - "numeric" if the value is an int, uint or a float
   398  //  - "string" if the value is a string
   399  //  - "array" if the value is a slice
   400  //  - "file" if the value is a slice of "filesystem.File"
   401  //  - "unsupported" otherwise
   402  func GetFieldType(value interface{}) string {
   403  	return getFieldType(reflect.ValueOf(value))
   404  }
   405  
   406  func getFieldType(value reflect.Value) string {
   407  	kind := value.Kind().String()
   408  	switch {
   409  	case strings.HasPrefix(kind, "int"), strings.HasPrefix(kind, "uint") && kind != "uintptr", strings.HasPrefix(kind, "float"):
   410  		return "numeric"
   411  	case kind == "string":
   412  		return "string"
   413  	case kind == "slice":
   414  		if value.Type().String() == "[]filesystem.File" {
   415  			return "file"
   416  		}
   417  		return "array"
   418  	default:
   419  		return "unsupported"
   420  	}
   421  }
   422  
   423  func parseRule(rule string) *Rule {
   424  	indexName := strings.Index(rule, ":")
   425  	params := []string{}
   426  	arrayDimensions := uint8(0)
   427  	var ruleName string
   428  	if indexName == -1 {
   429  		if strings.Count(rule, ",") > 0 {
   430  			panic(fmt.Sprintf("Invalid rule: \"%s\"", rule))
   431  		}
   432  		ruleName = rule
   433  	} else {
   434  		ruleName = rule[:indexName]
   435  		params = strings.Split(rule[indexName+1:], ",")
   436  	}
   437  
   438  	if ruleName[0] == '>' {
   439  		for ruleName[0] == '>' {
   440  			ruleName = ruleName[1:]
   441  			arrayDimensions++
   442  		}
   443  	}
   444  
   445  	return &Rule{ruleName, params, arrayDimensions}
   446  }