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 }