diff --git a/baked_in.go b/baked_in.go index f9a0245..c749266 100644 --- a/baked_in.go +++ b/baked_in.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. diff --git a/util.go b/util.go index 641c2d4..dbe897d 100644 --- a/util.go +++ b/util.go @@ -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 +} diff --git a/validator.go b/validator.go index 3542569..40f7e32 100644 --- a/validator.go +++ b/validator.go @@ -81,10 +81,12 @@ type Validate struct { // Config contains the options that a Validator instance will use. // It is passed to the New() function type Config struct { - TagName string - ValidationFuncs map[string]Func - CustomTypeFuncs map[reflect.Type]CustomTypeFunc - hasCustomFuncs bool + 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) } diff --git a/validator_test.go b/validator_test.go index 22084b4..91d8324 100644 --- a/validator_test.go +++ b/validator_test.go @@ -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"`