From 784a3cb8e56c0a996c247d33dcee389c5e614d05 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Mon, 11 Apr 2016 13:10:36 +0200 Subject: [PATCH 1/8] Fixed various code smells and coding style that the static analyzers found. --- arrays.go | 4 ++-- arrays_test.go | 2 +- error.go | 6 ++++-- numerics_test.go | 18 +++++++++--------- types.go | 1 + utils.go | 5 ++--- validator.go | 6 +++--- 7 files changed, 22 insertions(+), 20 deletions(-) diff --git a/arrays.go b/arrays.go index 55b8aac..5bace26 100644 --- a/arrays.go +++ b/arrays.go @@ -18,7 +18,7 @@ func Each(array []interface{}, iterator Iterator) { // Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result. func Map(array []interface{}, iterator ResultIterator) []interface{} { - var result []interface{} = make([]interface{}, len(array)) + var result = make([]interface{}, len(array)) for index, data := range array { result[index] = iterator(data, index) } @@ -37,7 +37,7 @@ func Find(array []interface{}, iterator ConditionIterator) interface{} { // Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice. func Filter(array []interface{}, iterator ConditionIterator) []interface{} { - var result []interface{} = make([]interface{}, 0) + var result = make([]interface{}, 0) for index, data := range array { if iterator(data, index) { result = append(result, data) diff --git a/arrays_test.go b/arrays_test.go index 8f61fc1..1a9ac66 100644 --- a/arrays_test.go +++ b/arrays_test.go @@ -78,7 +78,7 @@ func TestFilter(t *testing.T) { return value.(int)%2 == 0 } result := Filter(data, fn) - for i, _ := range result { + for i := range result { if result[i] != answer[i] { t.Errorf("Expected Filter(..) to be %v, got %v", answer[i], result[i]) } diff --git a/error.go b/error.go index 55d5fca..280b1c4 100644 --- a/error.go +++ b/error.go @@ -1,7 +1,9 @@ package govalidator +// Errors is an array of multiple errors and conforms to the error interface. type Errors []error +// Errors returns itself. func (es Errors) Errors() []error { return es } @@ -14,6 +16,7 @@ func (es Errors) Error() string { return err } +// Error encapsulates a name, an error and whether there's a custom error message or not. type Error struct { Name string Err error @@ -23,7 +26,6 @@ type Error struct { func (e Error) Error() string { if e.CustomErrorMessageExists { return e.Err.Error() - } else { - return e.Name + ": " + e.Err.Error() } + return e.Name + ": " + e.Err.Error() } diff --git a/numerics_test.go b/numerics_test.go index 1de43b4..1bad521 100644 --- a/numerics_test.go +++ b/numerics_test.go @@ -19,7 +19,7 @@ func TestAbs(t *testing.T) { for _, test := range tests { actual := Abs(test.param) if actual != test.expected { - t.Errorf("Expected Abs(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected Abs(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -41,7 +41,7 @@ func TestSign(t *testing.T) { for _, test := range tests { actual := Sign(test.param) if actual != test.expected { - t.Errorf("Expected Sign(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected Sign(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -63,7 +63,7 @@ func TestIsNegative(t *testing.T) { for _, test := range tests { actual := IsNegative(test.param) if actual != test.expected { - t.Errorf("Expected IsNegative(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsNegative(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -85,7 +85,7 @@ func TestIsNonNegative(t *testing.T) { for _, test := range tests { actual := IsNonNegative(test.param) if actual != test.expected { - t.Errorf("Expected IsNonNegative(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsNonNegative(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -107,7 +107,7 @@ func TestIsPositive(t *testing.T) { for _, test := range tests { actual := IsPositive(test.param) if actual != test.expected { - t.Errorf("Expected IsPositive(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsPositive(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -129,7 +129,7 @@ func TestIsNonPositive(t *testing.T) { for _, test := range tests { actual := IsNonPositive(test.param) if actual != test.expected { - t.Errorf("Expected IsNonPositive(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsNonPositive(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -151,7 +151,7 @@ func TestIsWhole(t *testing.T) { for _, test := range tests { actual := IsWhole(test.param) if actual != test.expected { - t.Errorf("Expected IsWhole(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsWhole(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -173,7 +173,7 @@ func TestIsNatural(t *testing.T) { for _, test := range tests { actual := IsNatural(test.param) if actual != test.expected { - t.Errorf("Expected IsNatural(%q) to be %v, got %v", test.param, test.expected, actual) + t.Errorf("Expected IsNatural(%v) to be %v, got %v", test.param, test.expected, actual) } } } @@ -198,7 +198,7 @@ func TestInRange(t *testing.T) { for _, test := range tests { actual := InRange(test.param, test.left, test.right) if actual != test.expected { - t.Errorf("Expected InRange(%q, %q, %q) to be %v, got %v", test.param, test.left, test.right, test.expected, actual) + t.Errorf("Expected InRange(%v, %v, %v) to be %v, got %v", test.param, test.left, test.right, test.expected, actual) } } } diff --git a/types.go b/types.go index abbe3d1..6f797a3 100644 --- a/types.go +++ b/types.go @@ -31,6 +31,7 @@ var ParamTagMap = map[string]ParamValidator{ "matches": StringMatches, } +// ParamTagRegexMap maps param tags to their respective regexes. var ParamTagRegexMap = map[string]*regexp.Regexp{ "length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"), "stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"), diff --git a/utils.go b/utils.go index 0eff62f..200b913 100644 --- a/utils.go +++ b/utils.go @@ -189,7 +189,7 @@ func NormalizeEmail(str string) (string, error) { return strings.Join(parts, "@"), nil } -// Will truncate a string closest length without breaking words. +// Truncate a string to the closest length without breaking words. func Truncate(str string, length int, ending string) string { var aftstr, befstr string if len(str) > length { @@ -203,9 +203,8 @@ func Truncate(str string, length int, ending string) string { if present > length && i != 0 { if (length - before) < (present - length) { return Trim(befstr, " /\\.,\"'#!?&@+-") + ending - } else { - return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending } + return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending } } } diff --git a/validator.go b/validator.go index 3772d59..76c545c 100644 --- a/validator.go +++ b/validator.go @@ -552,9 +552,9 @@ func ValidateStruct(s interface{}) (bool, error) { if typeField.PkgPath != "" { continue // Private field } - resultField, err := typeCheck(valueField, typeField) - if err != nil { - errs = append(errs, err) + resultField, err2 := typeCheck(valueField, typeField, val) + if err2 != nil { + errs = append(errs, err2) } result = result && resultField } From 1993fb40d6804a9e153d53916cb5d986a311444e Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Mon, 11 Apr 2016 13:19:04 +0200 Subject: [PATCH 2/8] [BC break] Enhanced custom type validators: the object being validated (in case of a struct) is passed as a second argument to the custom type validator function, making it possible to consider the whole object. --- types.go | 3 ++- validator.go | 10 +++++----- validator_test.go | 13 ++++++++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/types.go b/types.go index 6f797a3..0ec33a1 100644 --- a/types.go +++ b/types.go @@ -9,7 +9,8 @@ import ( type Validator func(str string) bool // CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type. -type CustomTypeValidator func(i interface{}) bool +// The second parameter should be the context (in the case of validating a struct: the whole object being validated). +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 diff --git a/validator.go b/validator.go index 76c545c..0e2105f 100644 --- a/validator.go +++ b/validator.go @@ -680,7 +680,7 @@ func checkRequired(v reflect.Value, t reflect.StructField, options tagOptions) ( return true, nil } -func typeCheck(v reflect.Value, t reflect.StructField) (bool, error) { +func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, error) { var customErrorMessageExists bool if !v.IsValid() { return false, nil @@ -709,7 +709,7 @@ func typeCheck(v reflect.Value, t reflect.StructField) (bool, error) { } if validatefunc, ok := CustomTypeTagMap[tagOptions[0]]; ok { options = append(options[:i], options[i+1:]...) // we found our custom validator, so remove it from the options - if result := validatefunc(v.Interface()); !result { + if result := validatefunc(v.Interface(), o.Interface()); !result { if len(tagOptions) == 2 { return false, Error{t.Name, fmt.Errorf(tagOptions[1]), true} } @@ -834,7 +834,7 @@ func typeCheck(v reflect.Value, t reflect.StructField) (bool, error) { var resultItem bool var err error if v.Index(i).Kind() != reflect.Struct { - resultItem, err = typeCheck(v.Index(i), t) + resultItem, err = typeCheck(v.Index(i), t, o) if err != nil { return false, err } @@ -853,7 +853,7 @@ func typeCheck(v reflect.Value, t reflect.StructField) (bool, error) { var resultItem bool var err error if v.Index(i).Kind() != reflect.Struct { - resultItem, err = typeCheck(v.Index(i), t) + resultItem, err = typeCheck(v.Index(i), t, o) if err != nil { return false, err } @@ -877,7 +877,7 @@ func typeCheck(v reflect.Value, t reflect.StructField) (bool, error) { if v.IsNil() { return true, nil } - return typeCheck(v.Elem(), t) + return typeCheck(v.Elem(), t, o) case reflect.Struct: return ValidateStruct(v.Interface()) default: diff --git a/validator_test.go b/validator_test.go index 5144f2e..f1c105a 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1930,7 +1930,18 @@ type StructWithCustomByteArray struct { func TestStructWithCustomByteArray(t *testing.T) { // add our custom byte array validator that fails when the byte array is pristine (all zeroes) - CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}) bool { + CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { + switch v := o.(type) { + case StructWithCustomByteArray: + if len(v.Email) > 0 { + if v.Email != "test@example.com" { + t.Errorf("v.Email should have been 'test@example.com' but was '%s'", v.Email) + } + } + default: + t.Errorf("Context object passed to custom validator should have been a StructWithCustomByteArray but was %T (%+v)", o, o) + } + switch v := i.(type) { case CustomByteArray: for _, e := range v { // check if v is empty, i.e. all zeroes From 7a07a97ed3257ebefc702b930c21827be5fe39c1 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Mon, 11 Apr 2016 13:20:23 +0200 Subject: [PATCH 3/8] Fixed a few URIs/URLs that were no longer passing validation (see #118 for details). --- validator_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/validator_test.go b/validator_test.go index f1c105a..c947cbd 100644 --- a/validator_test.go +++ b/validator_test.go @@ -647,7 +647,7 @@ func TestIsRequestURL(t *testing.T) { expected bool }{ {"", false}, - {"http://foo.bar#com", true}, + {"http://foo.bar/#com", true}, {"http://foobar.com", true}, {"https://foobar.com", true}, {"foobar.com", false}, @@ -670,7 +670,7 @@ func TestIsRequestURL(t *testing.T) { {"rtmp://foobar.com", true}, {"http://www.foo_bar.com/", true}, {"http://localhost:3000/", true}, - {"http://foobar.com#baz=qux", true}, + {"http://foobar.com/#baz=qux", true}, {"http://foobar.com/t$-_.+!*\\'(),", true}, {"http://www.foobar.com/~foobar", true}, {"http://www.-foobar.com/", true}, @@ -697,7 +697,7 @@ func TestIsRequestURI(t *testing.T) { expected bool }{ {"", false}, - {"http://foo.bar#com", true}, + {"http://foo.bar/#com", true}, {"http://foobar.com", true}, {"https://foobar.com", true}, {"foobar.com", false}, @@ -719,7 +719,7 @@ func TestIsRequestURI(t *testing.T) { {"rtmp://foobar.com", true}, {"http://www.foo_bar.com/", true}, {"http://localhost:3000/", true}, - {"http://foobar.com#baz=qux", true}, + {"http://foobar.com/#baz=qux", true}, {"http://foobar.com/t$-_.+!*\\'(),", true}, {"http://www.foobar.com/~foobar", true}, {"http://www.-foobar.com/", true}, From 40e33e2858f06a3c205f8b3344f27da132ea5361 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Mon, 2 May 2016 16:33:59 +0200 Subject: [PATCH 4/8] Upped the test coverage to 90% --- converter_test.go | 23 ++++++++++++++++++++++- error_test.go | 29 +++++++++++++++++++++++++++++ utils_test.go | 2 ++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 error_test.go diff --git a/converter_test.go b/converter_test.go index bb70279..875560e 100644 --- a/converter_test.go +++ b/converter_test.go @@ -1,6 +1,9 @@ package govalidator -import "testing" +import ( + "fmt" + "testing" +) func TestToInt(t *testing.T) { tests := []string{"1000", "-123", "abcdef", "100000000000000000000000000000000000000000000"} @@ -55,3 +58,21 @@ func TestToFloat(t *testing.T) { } } } + +func TestToJSON(t *testing.T) { + tests := []interface{}{"test", map[string]string{"a": "b", "b": "c"}, func() error { return fmt.Errorf("Error") }} + expected := [][]string{ + []string{"\"test\"", ""}, + []string{"{\"a\":\"b\",\"b\":\"c\"}", ""}, + []string{"", "json: unsupported type: func() error"}, + } + for i, test := range tests { + actual, err := ToJSON(test) + if actual != expected[i][0] { + t.Errorf("Expected toJSON(%v) to return '%v', got '%v'", test, expected[i][0], actual) + } + if fmt.Sprintf("%v", err) != expected[i][1] { + t.Errorf("Expected error returned from toJSON(%v) to return '%v', got '%v'", test, expected[i][1], fmt.Sprintf("%v", err)) + } + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..274cc0d --- /dev/null +++ b/error_test.go @@ -0,0 +1,29 @@ +package govalidator + +import ( + "fmt" + "testing" +) + +func TestErrorsToString(t *testing.T) { + t.Parallel() + customErr := &Error{Name: "Custom Error Name", Err: fmt.Errorf("stdlib error")} + customErrWithCustomErrorMessage := &Error{Name: "Custom Error Name 2", Err: fmt.Errorf("Bad stuff happened"), CustomErrorMessageExists: true} + + var tests = []struct { + param1 Errors + expected string + }{ + {Errors{}, ""}, + {Errors{fmt.Errorf("Error 1")}, "Error 1;"}, + {Errors{fmt.Errorf("Error 1"), fmt.Errorf("Error 2")}, "Error 1;Error 2;"}, + {Errors{customErr, fmt.Errorf("Error 2")}, "Custom Error Name: stdlib error;Error 2;"}, + {Errors{fmt.Errorf("Error 123"), customErrWithCustomErrorMessage}, "Error 123;Bad stuff happened;"}, + } + for _, test := range tests { + actual := test.param1.Error() + if actual != test.expected { + t.Errorf("Expected Error() to return '%v', got '%v'", test.expected, actual) + } + } +} diff --git a/utils_test.go b/utils_test.go index be3e33c..4a7ec24 100644 --- a/utils_test.go +++ b/utils_test.go @@ -395,6 +395,7 @@ func TestNormalizeEmail(t *testing.T) { {`some.name.midd.lena.me.+extension@googlemail.com`, `somenamemiddlename@gmail.com`}, {`some.name+extension@unknown.com`, `some.name+extension@unknown.com`}, {`hans@m端ller.com`, `hans@m端ller.com`}, + {`hans`, ``}, } for _, test := range tests { actual, err := NormalizeEmail(test.param) @@ -416,6 +417,7 @@ func TestTruncate(t *testing.T) { {`Lorem ipsum dolor sit amet, consectetur adipiscing elit.`, 25, `...`, `Lorem ipsum dolor sit amet...`}, {`Measuring programming progress by lines of code is like measuring aircraft building progress by weight.`, 35, ` new born babies!`, `Measuring programming progress by new born babies!`}, {`Testestestestestestestestestest testestestestestestestestest`, 7, `...`, `Testestestestestestestestestest...`}, + {`Testing`, 7, `...`, `Testing`}, } for _, test := range tests { actual := Truncate(test.param1, test.param2, test.param3) From 1ba975b1f9a538fb71200c5c628d832265fdf255 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Tue, 3 May 2016 15:41:04 +0200 Subject: [PATCH 5/8] Invoke all custom validators (fixes #116) --- validator.go | 27 +++++++++++++++++---------- validator_test.go | 17 +++++++++++++++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/validator.go b/validator.go index 0e2105f..59a19d3 100644 --- a/validator.go +++ b/validator.go @@ -701,23 +701,30 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e } options := parseTag(tag) - for i := range options { - tagOpt := options[i] - tagOptions := strings.Split(tagOpt, "~") - if ok := isValidTag(tagOptions[0]); !ok { + 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[tagOptions[0]]; ok { - options = append(options[:i], options[i+1:]...) // we found our custom validator, so remove it from the options + if validatefunc, ok := CustomTypeTagMap[tagOpts[0]]; ok { + customTypeValidatorsExist = true if result := validatefunc(v.Interface(), o.Interface()); !result { - if len(tagOptions) == 2 { - return false, Error{t.Name, fmt.Errorf(tagOptions[1]), true} + if len(tagOpts) == 2 { + customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(tagOpts[1]), CustomErrorMessageExists: true}) + continue } - return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), tagOptions[0]), false} + customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), tagOpts[0]), CustomErrorMessageExists: false}) } - return true, nil } } + if customTypeValidatorsExist { + if len(customTypeErrors.Errors()) > 0 { + return false, customTypeErrors + } + return true, nil + } if isEmptyValue(v) { // an empty value is not validated, check only required diff --git a/validator_test.go b/validator_test.go index c947cbd..7bf1bd5 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1924,11 +1924,14 @@ func TestFieldsRequiredByDefaultButExemptOrOptionalStruct(t *testing.T) { type CustomByteArray [6]byte type StructWithCustomByteArray struct { - ID CustomByteArray `valid:"customByteArrayValidator"` - Email string `valid:"email"` + ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` + Email string `valid:"email"` + CustomMinLength int `valid:"-"` } func TestStructWithCustomByteArray(t *testing.T) { + t.Parallel() + // add our custom byte array validator that fails when the byte array is pristine (all zeroes) CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { switch v := o.(type) { @@ -1952,6 +1955,13 @@ func TestStructWithCustomByteArray(t *testing.T) { } return false }) + CustomTypeTagMap["customMinLengthValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { + switch v := o.(type) { + case StructWithCustomByteArray: + return len(v.ID) >= v.CustomMinLength + } + return false + }) testCustomByteArray := CustomByteArray{'1', '2', '3', '4', '5', '6'} var tests = []struct { param StructWithCustomByteArray @@ -1960,7 +1970,9 @@ func TestStructWithCustomByteArray(t *testing.T) { {StructWithCustomByteArray{}, false}, {StructWithCustomByteArray{Email: "test@example.com"}, false}, {StructWithCustomByteArray{ID: testCustomByteArray, Email: "test@example.com"}, true}, + {StructWithCustomByteArray{ID: testCustomByteArray, Email: "test@example.com", CustomMinLength: 7}, false}, } + SetFieldsRequiredByDefault(true) for _, test := range tests { actual, err := ValidateStruct(test.param) if actual != test.expected { @@ -1970,6 +1982,7 @@ func TestStructWithCustomByteArray(t *testing.T) { } } } + SetFieldsRequiredByDefault(false) } func TestValidateNegationStruct(t *testing.T) { From da9d480cc787f2be225428a3f607109d384d5a34 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Sun, 8 May 2016 09:36:14 +0200 Subject: [PATCH 6/8] Fix data race around accessing custom validators Because I already introduced a BC break, why not add another? The last build mysteriously failed due to a data race that happens when accessing the custom validators. You need to use the new `Get` and `Set` methods on the `CustomTypeTagMap` struct which protects its private validator type map with a mutex. --- types.go | 22 +++++++++++++++++++++- validator.go | 2 +- validator_test.go | 8 ++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/types.go b/types.go index 0ec33a1..5131beb 100644 --- a/types.go +++ b/types.go @@ -3,6 +3,7 @@ package govalidator import ( "reflect" "regexp" + "sync" ) // Validator is a wrapper for a validator function that returns bool and accepts string. @@ -39,10 +40,29 @@ var ParamTagRegexMap = map[string]*regexp.Regexp{ "matches": regexp.MustCompile(`matches\(([^)]+)\)`), } +type customTypeTagMap struct { + validators map[string]CustomTypeValidator + + sync.RWMutex +} + +func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) { + tm.RLock() + defer tm.RUnlock() + v, ok := tm.validators[name] + return v, ok +} + +func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) { + tm.Lock() + defer tm.Unlock() + tm.validators[name] = ctv +} + // CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function. // Use this to validate compound or custom types that need to be handled as a whole, e.g. // `type UUID [16]byte` (this would be handled as an array of bytes). -var CustomTypeTagMap = map[string]CustomTypeValidator{} +var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)} // TagMap is a map of functions, that can be used as tags for ValidateStruct function. var TagMap = map[string]Validator{ diff --git a/validator.go b/validator.go index 59a19d3..d0fec2c 100644 --- a/validator.go +++ b/validator.go @@ -708,7 +708,7 @@ func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value) (bool, e if ok := isValidTag(tagOpts[0]); !ok { continue } - if validatefunc, ok := CustomTypeTagMap[tagOpts[0]]; ok { + if validatefunc, ok := CustomTypeTagMap.Get(tagOpts[0]); ok { customTypeValidatorsExist = true if result := validatefunc(v.Interface(), o.Interface()); !result { if len(tagOpts) == 2 { diff --git a/validator_test.go b/validator_test.go index 7bf1bd5..2d6b771 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1933,7 +1933,7 @@ func TestStructWithCustomByteArray(t *testing.T) { t.Parallel() // add our custom byte array validator that fails when the byte array is pristine (all zeroes) - CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { + CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool { switch v := o.(type) { case StructWithCustomByteArray: if len(v.Email) > 0 { @@ -1954,14 +1954,14 @@ func TestStructWithCustomByteArray(t *testing.T) { } } return false - }) - CustomTypeTagMap["customMinLengthValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { + })) + CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool { switch v := o.(type) { case StructWithCustomByteArray: return len(v.ID) >= v.CustomMinLength } return false - }) + })) testCustomByteArray := CustomByteArray{'1', '2', '3', '4', '5', '6'} var tests = []struct { param StructWithCustomByteArray From 8a9a932746a7b460040693299ef0234b9f0f205b Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Tue, 17 May 2016 12:09:46 +0200 Subject: [PATCH 7/8] Add missing documentation for custom validators This adds annotated usage examples for custom validators and the recent breaking changes for registering and using them. --- README.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a08372..de9097b 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,68 @@ import "github.com/asaskevich/govalidator" If you unhappy to use long `govalidator`, you can do something like this: ```go import ( - valid "github.com/asaskevich/govalidator" + valid "github.com/asaskevich/govalidator" ) ``` +#### 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. + +```go +import "github.com/asaskevich/govalidator" + +func init() { + govalidator.SetFieldsRequiredByDefault(true) +} +``` + +Here's some code to explain it: +```go +// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter): +type exampleStruct struct { + Name string `` + Email string `valid:"email"` + +// this, however, will only fail when Email is empty or an invalid email address: +type exampleStruct2 struct { + Name string `valid:"-"` + Email string `valid:"email"` + +// lastly, this will only fail when Email is an invalid email address but not when it's empty: +type exampleStruct2 struct { + Name string `valid:"-"` + Email string `valid:"email,optional"` +``` + +#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123)) +##### Custom validator function signature +A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible. +```go +import "github.com/asaskevich/govalidator" + +// old signature +func(i interface{}) bool + +// new signature +func(i interface{}, o interface{}) bool +``` + +##### Adding a custom validator +This was changed to prevent data races when accessing custom validators. +```go +import "github.com/asaskevich/govalidator" + +// before +govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool { + // ... +}) + +// after +govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool { + // ... +})) +``` + #### List of functions: ```go func Abs(value float64) float64 @@ -184,6 +242,8 @@ govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool { return str == "duck" }) ``` +For completely custom validators (interface-based), see below. + Here is a list of available validators for struct fields (validator - used function): ```go "alpha": IsAlpha, @@ -272,6 +332,49 @@ println(result) println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa") ``` +###### Custom validation functions +Custom validation using your own domain specific validators is also available - here's an example of how to use it: +```go +import "github.com/asaskevich/govalidator" + +type CustomByteArray [6]byte // custom types are supported and can be validated + +type StructWithCustomByteArray struct { + ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence + Email string `valid:"email"` + CustomMinLength int `valid:"-"` +} + +govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool { + switch v := context.(type) { // you can type switch on the context interface being validated + case StructWithCustomByteArray: + // you can check and validate against some other field in the context, + // return early or not validate against the context at all – your choice + case SomeOtherType: + // ... + default: + // expecting some other type? Throw/panic here or continue + } + + switch v := i.(type) { // type switch on the struct field being validated + case CustomByteArray: + for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes + if e != 0 { + return true + } + } + } + return false +})) +govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool { + switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation + case StructWithCustomByteArray: + return len(v.ID) >= v.CustomMinLength + } + return false +})) +``` + #### Notes Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator). Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator). From c5b5a565836a55091c9c936e16534b52e7f9c1e2 Mon Sep 17 00:00:00 2001 From: Daniel Lohse Date: Tue, 17 May 2016 12:13:58 +0200 Subject: [PATCH 8/8] Fix minor English grammar mistake --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de9097b..ff488a3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Add following line in your `*.go` file: ```go import "github.com/asaskevich/govalidator" ``` -If you unhappy to use long `govalidator`, you can do something like this: +If you are unhappy to use long `govalidator`, you can do something like this: ```go import ( valid "github.com/asaskevich/govalidator"