Skip to content

Commit

Permalink
Merge pull request #132 from annismckenzie/validator_tag_options_map
Browse files Browse the repository at this point in the history
Validator tag options map
  • Loading branch information
asaskevich committed May 19, 2016
2 parents 68c9ffa + 8b0bfb3 commit df81827
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 89 deletions.
2 changes: 1 addition & 1 deletion types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 tagOptions []string
type tagOptionsMap map[string]string

// UnsupportedTypeError is a wrapper for reflect.Type
type UnsupportedTypeError struct {
Expand Down
130 changes: 48 additions & 82 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,11 +564,22 @@ func ValidateStruct(s interface{}) (bool, error) {
return result, err
}

// parseTag splits a struct field's tag into its
// comma-separated options.
func parseTag(tag string) tagOptions {
split := strings.SplitN(tag, ",", -1)
return tagOptions(split)
// parseTagIntoMap parses a struct tag `valid:required~Some error message,length(2|3)` into map[string]string{"required": "Some error message", "length(2|3)": ""}
func parseTagIntoMap(tag string) tagOptionsMap {
optionsMap := make(tagOptionsMap)
options := strings.SplitN(tag, ",", -1)
for _, option := range options {
validationOptions := strings.Split(option, "~")
if !isValidTag(validationOptions[0]) {
continue
}
if len(validationOptions) == 2 {
optionsMap[validationOptions[0]] = validationOptions[1]
} else {
optionsMap[validationOptions[0]] = ""
}
}
return optionsMap
}

func isValidTag(s string) bool {
Expand Down Expand Up @@ -636,52 +647,20 @@ func StringLength(str string, params ...string) bool {
return false
}

// Contains returns whether checks that a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (opts tagOptions) contains(optionName string) bool {
for i := range opts {
tagOpt := opts[i]
if tagOpt == optionName {
return true
}
}
return false
}

func searchOption(limit int, predicate func(counter int) bool) int {
for counter := 0; counter < limit; counter++ {
if predicate(counter) {
return counter
func checkRequired(v reflect.Value, t reflect.StructField, options tagOptionsMap) (bool, error) {
if requiredOption, isRequired := options["required"]; isRequired {
if len(requiredOption) > 0 {
return false, Error{t.Name, fmt.Errorf(requiredOption), true}
}
}
return -1
}

func checkRequired(v reflect.Value, t reflect.StructField, options tagOptions) (bool, error) {
var err error
var customErrorMessageExists bool
requiredIndex := searchOption(len(options), func(index int) bool { return strings.HasPrefix(options[index], "required") })
optionalIndex := searchOption(len(options), func(index int) bool { return strings.HasPrefix(options[index], "optional") })
if requiredIndex > -1 {
validationOptions := strings.Split(options[requiredIndex], "~")
if len(validationOptions) == 2 {
err = fmt.Errorf(strings.Split(options[requiredIndex], "~")[1])
customErrorMessageExists = true
} else {
err = fmt.Errorf("non zero value required")
}
return false, Error{t.Name, err, customErrorMessageExists}
} else if fieldsRequiredByDefault && optionalIndex == -1 {
err := fmt.Errorf("All fields are required to at least have one validation defined")
return false, Error{t.Name, err, customErrorMessageExists}
return false, Error{t.Name, fmt.Errorf("non zero value required"), false}
} else if _, isOptional := options["optional"]; fieldsRequiredByDefault && !isOptional {
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
}
// not required and empty is valid
return true, nil
}

func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, error) {
var customErrorMessageExists bool
if !v.IsValid() {
return false, nil
}
Expand All @@ -694,28 +673,23 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e
if !fieldsRequiredByDefault {
return true, nil
}
err := fmt.Errorf("All fields are required to at least have one validation defined")
return false, Error{t.Name, err, customErrorMessageExists}
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
case "-":
return true, nil
}

options := parseTag(tag)
options := parseTagIntoMap(tag)
var customTypeErrors Errors
var customTypeValidatorsExist bool
for _, tagOpt := range options {
tagOpts := strings.Split(tagOpt, "~")
if ok := isValidTag(tagOpts[0]); !ok {
continue
}
if validatefunc, ok := CustomTypeTagMap.Get(tagOpts[0]); ok {
for validatorName, customErrorMessage := range options {
if validatefunc, ok := CustomTypeTagMap.Get(validatorName); ok {
customTypeValidatorsExist = true
if result := validatefunc(v.Interface(), o.Interface()); !result {
if len(tagOpts) == 2 {
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(tagOpts[1]), CustomErrorMessageExists: true})
if len(customErrorMessage) > 0 {
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(customErrorMessage), CustomErrorMessageExists: true})
continue
}
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), tagOpts[0]), CustomErrorMessageExists: false})
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), validatorName), CustomErrorMessageExists: false})
}
}
}
Expand All @@ -738,25 +712,18 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e
reflect.Float32, reflect.Float64,
reflect.String:
// for each tag option check the map of validator functions
for i := range options {
tagOpt := options[i]
tagOptions := strings.Split(tagOpt, "~")
negate := false
customMsgExists := (len(tagOptions) == 2)
for validator, customErrorMessage := range options {
var negate bool
customMsgExists := (len(customErrorMessage) > 0)
// Check wether the tag looks like '!something' or 'something'
if len(tagOptions[0]) > 0 && tagOptions[0][0] == '!' {
tagOpt = string(tagOptions[0][1:])
tagOptions[0] = tagOpt
if validator[0] == '!' {
validator = string(validator[1:])
negate = true
}
if ok := isValidTag(tagOptions[0]); !ok {
err := fmt.Errorf("Unknown Validator %s", tagOptions[0])
return false, Error{t.Name, err, false}
}

// Check for param validators
for key, value := range ParamTagRegexMap {
ps := value.FindStringSubmatch(tagOptions[0])
ps := value.FindStringSubmatch(validator)
if len(ps) > 0 {
if validatefunc, ok := ParamTagMap[key]; ok {
switch v.Kind() {
Expand All @@ -766,30 +733,29 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e
var err error
if !negate {
if customMsgExists {
err = fmt.Errorf(tagOptions[1])
err = fmt.Errorf(customErrorMessage)
} else {
err = fmt.Errorf("%s does not validate as %s", field, tagOpt)
err = fmt.Errorf("%s does not validate as %s", field, validator)
}

} else {
if customMsgExists {
err = fmt.Errorf(tagOptions[1])
err = fmt.Errorf(customErrorMessage)
} else {
err = fmt.Errorf("%s does validate as %s", field, tagOpt)
err = fmt.Errorf("%s does validate as %s", field, validator)
}
}
return false, Error{t.Name, err, customMsgExists}
}
default:
//Not Yet Supported Types (Fail here!)
err := fmt.Errorf("Validator %s doesn't support kind %s", tagOptions[0], v.Kind())
return false, Error{t.Name, err, false}
// type not yet supported, fail
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false}
}
}
}
}

if validatefunc, ok := TagMap[tagOptions[0]]; ok {
if validatefunc, ok := TagMap[validator]; ok {
switch v.Kind() {
case reflect.String:
field := fmt.Sprint(v) // make value into string, then validate with regex
Expand All @@ -798,22 +764,22 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e

if !negate {
if customMsgExists {
err = fmt.Errorf(tagOptions[1])
err = fmt.Errorf(customErrorMessage)
} else {
err = fmt.Errorf("%s does not validate as %s", field, tagOpt)
err = fmt.Errorf("%s does not validate as %s", field, validator)
}
} else {
if customMsgExists {
err = fmt.Errorf(tagOptions[1])
err = fmt.Errorf(customErrorMessage)
} else {
err = fmt.Errorf("%s does validate as %s", field, tagOpt)
err = fmt.Errorf("%s does validate as %s", field, validator)
}
}
return false, Error{t.Name, err, customMsgExists}
}
default:
//Not Yet Supported Types (Fail here!)
err := fmt.Errorf("Validator %s doesn't support kind %s for value %v", tagOptions[0], v.Kind(), v)
err := fmt.Errorf("Validator %s doesn't support kind %s for value %v", validator, v.Kind(), v)
return false, Error{t.Name, err, false}
}
}
Expand Down
13 changes: 7 additions & 6 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1797,7 +1797,7 @@ type UserValid struct {
Password string `valid:"required"`
Age int `valid:"required"`
Home *Address
Work []Address
Work []Address `valid:"required"`
}

type PrivateStruct struct {
Expand Down Expand Up @@ -1833,7 +1833,7 @@ type Post struct {
AuthorIP string `valid:"ipv4"`
}

type MissingValidationDeclationStruct struct {
type MissingValidationDeclarationStruct struct {
Name string ``
Email string `valid:"required,email"`
}
Expand All @@ -1853,13 +1853,13 @@ type MessageWithSeveralFieldsStruct struct {
Body string `valid:"length(1|10)"`
}

func TestValidateMissingValidationDeclationStruct(t *testing.T) {
func TestValidateMissingValidationDeclarationStruct(t *testing.T) {
var tests = []struct {
param MissingValidationDeclationStruct
param MissingValidationDeclarationStruct
expected bool
}{
{MissingValidationDeclationStruct{}, false},
{MissingValidationDeclationStruct{Name: "TEST", Email: "test@example.com"}, false},
{MissingValidationDeclarationStruct{}, false},
{MissingValidationDeclarationStruct{Name: "TEST", Email: "test@example.com"}, false},
}
SetFieldsRequiredByDefault(true)
for _, test := range tests {
Expand Down Expand Up @@ -2087,6 +2087,7 @@ func TestValidateStruct(t *testing.T) {
{User{"John", "john!yahoo.com", "12345678", 20, &Address{"Street", "ABC456D89"}, []Address{Address{"Street", "ABC456D89"}, Address{"Street", "123456"}}}, false},
{User{"John", "", "12345", 0, &Address{"Street", "123456789"}, []Address{Address{"Street", "ABC456D89"}, Address{"Street", "123456"}}}, false},
{UserValid{"John", "john@yahoo.com", "123G#678", 20, &Address{"Street", "123456"}, []Address{Address{"Street", "123456"}, Address{"Street", "123456"}}}, true},
{UserValid{"John", "john!yahoo.com", "12345678", 20, &Address{"Street", "ABC456D89"}, []Address{}}, false},
{UserValid{"John", "john!yahoo.com", "12345678", 20, &Address{"Street", "ABC456D89"}, []Address{Address{"Street", "ABC456D89"}, Address{"Street", "123456"}}}, false},
{UserValid{"John", "", "12345", 0, &Address{"Street", "123456789"}, []Address{Address{"Street", "ABC456D89"}, Address{"Street", "123456"}}}, false},
{nil, true},
Expand Down

0 comments on commit df81827

Please sign in to comment.