Add Tag Alias + basic tests

so far so good
pull/169/head
joeybloggs 9 years ago
parent f6ff3d4da3
commit 2cda50fb41
  1. 8
      baked_in.go
  2. 61
      util.go
  3. 65
      validator.go
  4. 27
      validator_test.go

@ -10,6 +10,14 @@ import (
"unicode/utf8"
)
// BakedInAliasValidators is a default mapping of a single validationstag that
// defines a common or complex set of validation(s) to simplify
// adding validation to structs. i.e. set key "_ageok" and the tags
// are "gt=0,lte=130" or key "_preferredname" and tags "omitempty,gt=0,lte=60"
var BakedInAliasValidators = map[string]string{
"iscolor": "hexcolor|rgb|rgba|hsl|hsla",
}
// BakedInValidators is the default map of ValidationFunc
// you can add, remove or even replace items to suite your needs,
// or even disregard and use your own map if so desired.

@ -1,6 +1,7 @@
package validator
import (
"fmt"
"reflect"
"strconv"
"strings"
@ -10,6 +11,21 @@ const (
namespaceSeparator = "."
leftBracket = "["
rightBracket = "]"
restrictedTagChars = ".[],|=+()`~!@#$%^&*\\\"/?<>{}"
restrictedAliasErr = "Alias \"%s\" either contains restricted characters or is the same as a restricted tag needed for normal operation\n"
restrictedTagErr = "Tag \"%s\" either contains restricted characters or is the same as a restricted tag needed for normal operation\n"
)
var (
restrictedTags = map[string]*struct{}{
diveTag: emptyStructPtr,
existsTag: emptyStructPtr,
structOnlyTag: emptyStructPtr,
omitempty: emptyStructPtr,
skipValidationTag: emptyStructPtr,
utf8HexComma: emptyStructPtr,
utf8Pipe: emptyStructPtr,
}
)
func (v *Validate) extractType(current reflect.Value) (reflect.Value, reflect.Kind) {
@ -214,3 +230,48 @@ func panicIf(err error) {
panic(err.Error())
}
}
func (v *Validate) parseTags(tag, fieldName string) []*tagCache {
tags := []*tagCache{}
for _, t := range strings.Split(tag, tagSeparator) {
if v.config.hasAliasValidators {
// check map for alias and process new tags, otherwise process as usual
if tagsVal, ok := v.config.AliasValidators[t]; ok {
return v.parseTags(tagsVal, fieldName)
}
}
if t == diveTag {
tags = append(tags, &tagCache{tagVals: [][]string{{t}}})
break
}
// if a pipe character is needed within the param you must use the utf8Pipe representation "0x7C"
orVals := strings.Split(t, orSeparator)
cTag := &tagCache{isOrVal: len(orVals) > 1, tagVals: make([][]string, len(orVals))}
tags = append(tags, cTag)
var key string
var param string
for i, val := range orVals {
vals := strings.SplitN(val, tagKeySeparator, 2)
key = vals[0]
if len(key) == 0 {
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, fieldName)))
}
if len(vals) > 1 {
param = strings.Replace(strings.Replace(vals[1], utf8HexComma, ",", -1), utf8Pipe, "|", -1)
}
cTag.tagVals[i] = []string{key, param}
}
}
return tags
}

@ -84,7 +84,9 @@ type Config struct {
TagName string
ValidationFuncs map[string]Func
CustomTypeFuncs map[reflect.Type]CustomTypeFunc
AliasValidators map[string]string
hasCustomFuncs bool
hasAliasValidators bool
}
// CustomTypeFunc allows for overriding or adding custom field type handler functions
@ -139,6 +141,10 @@ func New(config Config) *Validate {
config.hasCustomFuncs = true
}
if config.AliasValidators != nil && len(config.AliasValidators) > 0 {
config.hasAliasValidators = true
}
return &Validate{config: config}
}
@ -155,6 +161,12 @@ func (v *Validate) RegisterValidation(key string, f Func) error {
return errors.New("Function cannot be empty")
}
_, ok := restrictedTags[key]
if ok || strings.ContainsAny(key, restrictedTagChars) {
panic(fmt.Sprintf(restrictedTagErr, key))
}
v.config.ValidationFuncs[key] = f
return nil
@ -174,6 +186,25 @@ func (v *Validate) RegisterCustomTypeFunc(fn CustomTypeFunc, types ...interface{
v.config.hasCustomFuncs = true
}
// RegisterAliasValidation registers a mapping of a single validationstag that
// defines a common or complex set of validation(s) to simplify adding validation
// to structs.
func (v *Validate) RegisterAliasValidation(alias, tags string) {
if v.config.AliasValidators == nil {
v.config.AliasValidators = map[string]string{}
}
_, ok := restrictedTags[alias]
if ok || strings.ContainsAny(alias, restrictedTagChars) {
panic(fmt.Sprintf(restrictedAliasErr, alias))
}
v.config.AliasValidators[alias] = tags
v.config.hasAliasValidators = true
}
// Field validates a single field using tag style validation and returns ValidationErrors
// NOTE: it returns ValidationErrors instead of a single FieldError because this can also
// validate Array, Slice and maps fields which may contain more than one error
@ -430,39 +461,7 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.
tags, isCached := tagsCache.Get(tag)
if !isCached {
tags = []*tagCache{}
for _, t := range strings.Split(tag, tagSeparator) {
if t == diveTag {
tags = append(tags, &tagCache{tagVals: [][]string{{t}}})
break
}
// if a pipe character is needed within the param you must use the utf8Pipe representation "0x7C"
orVals := strings.Split(t, orSeparator)
cTag := &tagCache{isOrVal: len(orVals) > 1, tagVals: make([][]string, len(orVals))}
tags = append(tags, cTag)
var key string
var param string
for i, val := range orVals {
vals := strings.SplitN(val, tagKeySeparator, 2)
key = vals[0]
if len(key) == 0 {
panic(strings.TrimSpace(fmt.Sprintf(invalidValidation, name)))
}
if len(vals) > 1 {
param = strings.Replace(strings.Replace(vals[1], utf8HexComma, ",", -1), utf8Pipe, "|", -1)
}
cTag.tagVals[i] = []string{key, param}
}
}
tags = v.parseTags(tag, name)
tagsCache.Set(tag, tags)
}

@ -111,7 +111,7 @@ type TestSlice struct {
OmitEmpty []int `validate:"omitempty,min=1,max=10"`
}
var validate = New(Config{TagName: "validate", ValidationFuncs: BakedInValidators})
var validate = New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, AliasValidators: BakedInAliasValidators})
func AssertError(t *testing.T, errs ValidationErrors, key, field, expectedTag string) {
@ -210,6 +210,24 @@ type TestPartial struct {
}
}
func TestAliasTags(t *testing.T) {
s := "rgb(255,255,255)"
errs := validate.Field(s, "iscolor")
Equal(t, errs, nil)
type Test struct {
Color string `vlidate:"iscolor"`
}
tst := &Test{
Color: "#000",
}
errs = validate.Struct(tst)
Equal(t, errs, nil)
}
func TestStructPartial(t *testing.T) {
p1 := []string{
@ -1805,7 +1823,8 @@ func TestMapDiveValidation(t *testing.T) {
AssertError(t, errs, "TestMapStruct.Errs[3].Name", "Name", "required")
// for full test coverage
fmt.Sprint(errs.Error())
s := fmt.Sprint(errs.Error())
NotEqual(t, s, "")
type TestMapTimeStruct struct {
Errs map[int]*time.Time `validate:"gt=0,dive,required"`
@ -1968,8 +1987,10 @@ func TestArrayDiveValidation(t *testing.T) {
AssertError(t, errs, "TestMultiDimensionalStructsPtr.Errs[1][1].Name", "Name", "required")
AssertError(t, errs, "TestMultiDimensionalStructsPtr.Errs[1][2].Name", "Name", "required")
AssertError(t, errs, "TestMultiDimensionalStructsPtr.Errs[2][1].Name", "Name", "required")
// for full test coverage
fmt.Sprint(errs.Error())
s := fmt.Sprint(errs.Error())
NotEqual(t, s, "")
type TestMultiDimensionalStructsPtr2 struct {
Errs [][]*Inner `validate:"gt=0,dive,dive,required"`

Loading…
Cancel
Save