Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include path for nested structs #290

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ govalidator
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator) [![Coverage Status](https://img.shields.io/coveralls/asaskevich/govalidator.svg)](https://coveralls.io/r/asaskevich/govalidator?branch=master) [![wercker status](https://app.wercker.com/status/1ec990b09ea86c910d5f08b0e02c6043/s "wercker status")](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [![Backers on Open Collective](https://opencollective.com/govalidator/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/govalidator/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_shield)


A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).

#### Installation
Expand Down Expand Up @@ -34,6 +33,8 @@ import (
#### Activate behavior to require all fields have a validation tag by default
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.

`SetNilPtrAllowedByRequired` causes validation to pass when struct fields marked by `required` are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between `nil` and `zero value` state can use this. If disabled, both `nil` and `zero` values cause validation errors.

```go
import "github.com/asaskevich/govalidator"

Expand Down
9 changes: 8 additions & 1 deletion error.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,18 @@ type Error struct {

// Validator indicates the name of the validator that failed
Validator string
Path []string
}

func (e Error) Error() string {
if e.CustomErrorMessageExists {
return e.Err.Error()
}
return e.Name + ": " + e.Err.Error()

errName := e.Name
if len(e.Path) > 0 {
errName = strings.Join(append(e.Path, e.Name), ".")
}

return errName + ": " + e.Err.Error()
}
22 changes: 21 additions & 1 deletion types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package govalidator
import (
"reflect"
"regexp"
"sort"
"sync"
)

Expand All @@ -15,7 +16,26 @@ type CustomTypeValidator func(i interface{}, o interface{}) bool

// ParamValidator is a wrapper for validator functions that accepts additional parameters.
type ParamValidator func(str string, params ...string) bool
type tagOptionsMap map[string]string
type tagOptionsMap map[string]tagOption

func (t tagOptionsMap) orderedKeys() []string {
var keys []string
for k := range t {
keys = append(keys, k)
}

sort.Slice(keys, func(a, b int) bool {
return t[keys[a]].order < t[keys[b]].order
})

return keys
}

type tagOption struct {
name string
customErrorMessage string
order int
}

// UnsupportedTypeError is a wrapper for reflect.Type
type UnsupportedTypeError struct {
Expand Down
6 changes: 6 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,9 @@ func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight b

return leftSide + str + rightSide
}

// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
func TruncatingErrorf(str string, args ...interface{}) error {
n := strings.Count(str, "%s")
return fmt.Errorf(str, args[:n]...)
}
102 changes: 75 additions & 27 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

var (
fieldsRequiredByDefault bool
nilPtrAllowedByRequired = false
notNumberRegexp = regexp.MustCompile("[^0-9]+")
whiteSpacesAndMinus = regexp.MustCompile(`[\s-]+`)
paramsRegexp = regexp.MustCompile(`\(.*\)$`)
Expand Down Expand Up @@ -51,6 +52,17 @@ func SetFieldsRequiredByDefault(value bool) {
fieldsRequiredByDefault = value
}

// SetNilPtrAllowedByRequired causes validation to pass for nil ptrs when a field is set to required.
// The validation will still reject ptr fields in their zero value state. Example with this enabled:
// type exampleStruct struct {
// Name *string `valid:"required"`
// With `Name` set to "", this will be considered invalid input and will cause a validation error.
// With `Name` set to nil, this will be considered valid by validation.
// By default this is disabled.
func SetNilPtrAllowedByRequired(value bool) {
nilPtrAllowedByRequired = value
}

// IsEmail check if the string is an email.
func IsEmail(str string) bool {
// TODO uppercase letters are not supported
Expand Down Expand Up @@ -705,6 +717,22 @@ func toJSONName(tag string) string {
return name
}

func PrependPathToErrors(err error, path string) error {
switch err2 := err.(type) {
case Error:
err2.Path = append([]string{path}, err2.Path...)
return err2
case Errors:
errors := err2.Errors()
for i, err3 := range errors {
errors[i] = PrependPathToErrors(err3, path)
}
return err2
}
fmt.Println(err)
return err
}

// ValidateStruct use tags for fields.
// result will be equal to `false` if there are any errors.
func ValidateStruct(s interface{}) (bool, error) {
Expand Down Expand Up @@ -738,6 +766,7 @@ func ValidateStruct(s interface{}) (bool, error) {
var err error
structResult, err = ValidateStruct(valueField.Interface())
if err != nil {
err = PrependPathToErrors(err, typeField.Name)
errs = append(errs, err)
}
}
Expand Down Expand Up @@ -779,17 +808,17 @@ func parseTagIntoMap(tag string) tagOptionsMap {
optionsMap := make(tagOptionsMap)
options := strings.Split(tag, ",")

for _, option := range options {
for i, option := range options {
option = strings.TrimSpace(option)

validationOptions := strings.Split(option, "~")
if !isValidTag(validationOptions[0]) {
continue
}
if len(validationOptions) == 2 {
optionsMap[validationOptions[0]] = validationOptions[1]
optionsMap[validationOptions[0]] = tagOption{validationOptions[0], validationOptions[1], i}
} else {
optionsMap[validationOptions[0]] = ""
optionsMap[validationOptions[0]] = tagOption{validationOptions[0], "", i}
}
}
return optionsMap
Expand Down Expand Up @@ -940,13 +969,20 @@ func IsIn(str string, params ...string) bool {
}

func checkRequired(v reflect.Value, t reflect.StructField, options tagOptionsMap) (bool, error) {
if nilPtrAllowedByRequired {
k := v.Kind()
if (k == reflect.Ptr || k == reflect.Interface) && v.IsNil() {
return true, nil
}
}

if requiredOption, isRequired := options["required"]; isRequired {
if len(requiredOption) > 0 {
return false, Error{t.Name, fmt.Errorf(requiredOption), true, "required"}
if len(requiredOption.customErrorMessage) > 0 {
return false, Error{t.Name, fmt.Errorf(requiredOption.customErrorMessage), true, "required", []string{}}
}
return false, Error{t.Name, fmt.Errorf("non zero value required"), false, "required"}
return false, Error{t.Name, fmt.Errorf("non zero value required"), false, "required", []string{}}
} else if _, isOptional := options["optional"]; fieldsRequiredByDefault && !isOptional {
return false, Error{t.Name, fmt.Errorf("Missing required field"), false, "required"}
return false, Error{t.Name, fmt.Errorf("Missing required field"), false, "required", []string{}}
}
// not required and empty is valid
return true, nil
Expand All @@ -962,10 +998,12 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
// Check if the field should be ignored
switch tag {
case "":
if !fieldsRequiredByDefault {
return true, nil
if v.Kind() != reflect.Slice && v.Kind() != reflect.Map {
if !fieldsRequiredByDefault {
return true, nil
}
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false, "required", []string{}}
}
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false, "required"}
case "-":
return true, nil
}
Expand All @@ -978,17 +1016,23 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options

if isEmptyValue(v) {
// an empty value is not validated, check only required
return checkRequired(v, t, options)
isValid, resultErr = checkRequired(v, t, options)
for key := range options {
delete(options, key)
}
return isValid, resultErr
}

var customTypeErrors Errors
for validatorName, customErrorMessage := range options {
optionsOrder := options.orderedKeys()
for _, validatorName := range optionsOrder {
validatorStruct := options[validatorName]
if validatefunc, ok := CustomTypeTagMap.Get(validatorName); ok {
delete(options, validatorName)

if result := validatefunc(v.Interface(), o.Interface()); !result {
if len(customErrorMessage) > 0 {
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(customErrorMessage), CustomErrorMessageExists: true, Validator: stripParams(validatorName)})
if len(validatorStruct.customErrorMessage) > 0 {
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: TruncatingErrorf(validatorStruct.customErrorMessage, fmt.Sprint(v), validatorName), CustomErrorMessageExists: true, Validator: stripParams(validatorName)})
continue
}
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), validatorName), CustomErrorMessageExists: false, Validator: stripParams(validatorName)})
Expand All @@ -1007,10 +1051,11 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
delete(options, "required")

if isValid && resultErr == nil && len(options) != 0 {
for validator := range options {
optionsOrder := options.orderedKeys()
for _, validator := range optionsOrder {
isValid = false
resultErr = Error{t.Name, fmt.Errorf(
"The following validator is invalid or can't be applied to the field: %q", validator), false, stripParams(validator)}
"The following validator is invalid or can't be applied to the field: %q", validator), false, stripParams(validator), []string{}}
return
}
}
Expand All @@ -1024,10 +1069,11 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
reflect.Float32, reflect.Float64,
reflect.String:
// for each tag option check the map of validator functions
for validatorSpec, customErrorMessage := range options {
for _, validatorSpec := range optionsOrder {
validatorStruct := options[validatorSpec]
var negate bool
validator := validatorSpec
customMsgExists := len(customErrorMessage) > 0
customMsgExists := len(validatorStruct.customErrorMessage) > 0

// Check whether the tag looks like '!something' or 'something'
if validator[0] == '!' {
Expand Down Expand Up @@ -1058,16 +1104,16 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
field := fmt.Sprint(v) // make value into string, then validate with regex
if result := validatefunc(field, ps[1:]...); (!result && !negate) || (result && negate) {
if customMsgExists {
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, TruncatingErrorf(validatorStruct.customErrorMessage, field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
if negate {
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
default:
// type not yet supported, fail
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false, stripParams(validatorSpec)}
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false, stripParams(validatorSpec), []string{}}
}
}

Expand All @@ -1079,17 +1125,17 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
field := fmt.Sprint(v) // make value into string, then validate with regex
if result := validatefunc(field); !result && !negate || result && negate {
if customMsgExists {
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, TruncatingErrorf(validatorStruct.customErrorMessage, field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
if negate {
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec)}
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists, stripParams(validatorSpec), []string{}}
}
default:
//Not Yet Supported Types (Fail here!)
err := fmt.Errorf("Validator %s doesn't support kind %s for value %v", validator, v.Kind(), v)
return false, Error{t.Name, err, false, stripParams(validatorSpec)}
return false, Error{t.Name, err, false, stripParams(validatorSpec), []string{}}
}
}
}
Expand All @@ -1102,7 +1148,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
sv = v.MapKeys()
sort.Sort(sv)
result := true
for _, k := range sv {
for i, k := range sv {
var resultItem bool
var err error
if v.MapIndex(k).Kind() != reflect.Struct {
Expand All @@ -1113,6 +1159,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
} else {
resultItem, err = ValidateStruct(v.MapIndex(k).Interface())
if err != nil {
err = PrependPathToErrors(err, t.Name+"."+sv[i].Interface().(string))
return false, err
}
}
Expand All @@ -1132,6 +1179,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options
} else {
resultItem, err = ValidateStruct(v.Index(i).Interface())
if err != nil {
err = PrependPathToErrors(err, t.Name+"."+strconv.Itoa(i))
return false, err
}
}
Expand Down