From d05e279aa37e473ba3d1e06f2e1a280570280c84 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Fri, 17 Jan 2025 13:46:06 +0100 Subject: [PATCH 01/13] add support to tag isvalid --- README.md | 2 ++ baked_in.go | 27 ++++++++++++++++ doc.go | 7 +++++ validator_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 25eadf026..d8439fd60 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,8 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | excluded_without | Excluded Without | | excluded_without_all | Excluded Without All | | unique | Unique | +| isvalid | Verify if the method `Validate() error` does not return an error | + #### Aliases: | Tag | Description | diff --git a/baked_in.go b/baked_in.go index 2f66c1836..7be7c8a5a 100644 --- a/baked_in.go +++ b/baked_in.go @@ -241,6 +241,7 @@ var ( "mongodb_connection_string": isMongoDBConnectionString, "cron": isCron, "spicedb": isSpiceDB, + "isvalid": isValid, } ) @@ -3044,3 +3045,29 @@ func isCron(fl FieldLevel) bool { cronString := fl.Field().String() return cronRegex().MatchString(cronString) } + +func isValid(fl FieldLevel) bool { + if instance, ok := tryConvertFieldTo[interface{ Validate() error }](fl.Field()); ok { + return instance.Validate() == nil + } + + return false +} + +func tryConvertFieldTo[V any](field reflect.Value) (v V, ok bool) { + v, ok = convertFieldTo[V](field) + if !ok && field.CanAddr() { + v, ok = convertFieldTo[V](field.Addr()) + } + + return v, ok +} + +func convertFieldTo[V any](field reflect.Value) (v V, ok bool) { + if v, ok = field.Interface().(V); ok { + return v, ok + } + + var zero V + return zero, false +} diff --git a/doc.go b/doc.go index c9b1616ee..a63ab2858 100644 --- a/doc.go +++ b/doc.go @@ -756,6 +756,13 @@ in a field of the struct specified via a parameter. // For slices of struct: Usage: unique=field +# IsValid + +This validates that an object respects the interface `Validate() error` and +the method `Validate` does not return an error. + + Usage: isvalid + # Alpha Only This validates that a string value contains ASCII alpha characters only diff --git a/validator_test.go b/validator_test.go index 5eadb2502..8e5a18061 100644 --- a/validator_test.go +++ b/validator_test.go @@ -7,6 +7,7 @@ import ( "database/sql/driver" "encoding/base64" "encoding/json" + "errors" "fmt" "image" "image/jpeg" @@ -12080,7 +12081,7 @@ func TestExcludedIf(t *testing.T) { test11 := struct { Field1 bool - Field2 *string `validate:"excluded_if=Field1 false"` + Field2 *string `validate:"excluded_if=Field1 false"` }{ Field1: false, Field2: nil, @@ -14123,3 +14124,78 @@ func TestPrivateFieldsStruct(t *testing.T) { Equal(t, len(errs), tc.errorNum) } } + +type NotRed struct { + Color string +} + +func (r *NotRed) Validate() error { + if r != nil && r.Color == "red" { + return errors.New("should not be red") + } + + return nil +} + +func TestIsValid(t *testing.T) { + t.Run("using pointer", func(t *testing.T) { + validate := New() + + type Test struct { + String string + Inner *NotRed `validate:"isvalid"` + } + + var tt Test + + errs := validate.Struct(tt) + NotEqual(t, errs, nil) + + fe := errs.(ValidationErrors)[0] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test.Inner") + Equal(t, fe.Tag(), "isvalid") + + tt.Inner = &NotRed{Color: "blue"} + errs = validate.Struct(tt) + Equal(t, errs, nil) + + tt.Inner = &NotRed{Color: "red"} + errs = validate.Struct(tt) + NotEqual(t, errs, nil) + + fe = errs.(ValidationErrors)[0] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test.Inner") + Equal(t, fe.Tag(), "isvalid") + + }) + + t.Run("using struct", func(t *testing.T) { + validate := New() + + type Test2 struct { + String string + Inner NotRed `validate:"isvalid"` + } + + var tt2 Test2 + + errs := validate.Struct(&tt2) + Equal(t, errs, nil) + + tt2.Inner = NotRed{Color: "blue"} + + errs = validate.Struct(&tt2) + Equal(t, errs, nil) + + tt2.Inner = NotRed{Color: "red"} + errs = validate.Struct(&tt2) + NotEqual(t, errs, nil) + + fe := errs.(ValidationErrors)[0] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test2.Inner") + Equal(t, fe.Tag(), "isvalid") + }) +} From db69b78b5d0e11691bc61245444245e47c679493 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Fri, 21 Mar 2025 17:16:24 +0100 Subject: [PATCH 02/13] rename tag from isValid to validateFn --- baked_in.go | 6 +++--- doc.go | 4 ++-- validator_test.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/baked_in.go b/baked_in.go index 1b72296e6..2bd7934d9 100644 --- a/baked_in.go +++ b/baked_in.go @@ -244,7 +244,7 @@ var ( "cron": isCron, "spicedb": isSpiceDB, "ein": isEIN, - "isvalid": isValid, + "validateFn": isValidateFn, } ) @@ -3079,7 +3079,7 @@ func isEIN(fl FieldLevel) bool { return einRegex().MatchString(field.String()) } -func isValid(fl FieldLevel) bool { +func isValidateFn(fl FieldLevel) bool { if instance, ok := tryConvertFieldTo[interface{ Validate() error }](fl.Field()); ok { return instance.Validate() == nil } @@ -3103,4 +3103,4 @@ func convertFieldTo[V any](field reflect.Value) (v V, ok bool) { var zero V return zero, false -} \ No newline at end of file +} diff --git a/doc.go b/doc.go index 01a6eed74..affef3110 100644 --- a/doc.go +++ b/doc.go @@ -756,12 +756,12 @@ in a field of the struct specified via a parameter. // For slices of struct: Usage: unique=field -# IsValid +# ValidateFn This validates that an object respects the interface `Validate() error` and the method `Validate` does not return an error. - Usage: isvalid + Usage: validateFn # Alpha Only diff --git a/validator_test.go b/validator_test.go index 706f78b6b..6506bb083 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14236,7 +14236,7 @@ func TestIsValid(t *testing.T) { type Test struct { String string - Inner *NotRed `validate:"isvalid"` + Inner *NotRed `validate:"validateFn"` } var tt Test @@ -14247,7 +14247,7 @@ func TestIsValid(t *testing.T) { fe := errs.(ValidationErrors)[0] Equal(t, fe.Field(), "Inner") Equal(t, fe.Namespace(), "Test.Inner") - Equal(t, fe.Tag(), "isvalid") + Equal(t, fe.Tag(), "validateFn") tt.Inner = &NotRed{Color: "blue"} errs = validate.Struct(tt) @@ -14260,7 +14260,7 @@ func TestIsValid(t *testing.T) { fe = errs.(ValidationErrors)[0] Equal(t, fe.Field(), "Inner") Equal(t, fe.Namespace(), "Test.Inner") - Equal(t, fe.Tag(), "isvalid") + Equal(t, fe.Tag(), "validateFn") }) @@ -14269,7 +14269,7 @@ func TestIsValid(t *testing.T) { type Test2 struct { String string - Inner NotRed `validate:"isvalid"` + Inner NotRed `validate:"validateFn"` } var tt2 Test2 @@ -14289,6 +14289,6 @@ func TestIsValid(t *testing.T) { fe := errs.(ValidationErrors)[0] Equal(t, fe.Field(), "Inner") Equal(t, fe.Namespace(), "Test2.Inner") - Equal(t, fe.Tag(), "isvalid") + Equal(t, fe.Tag(), "validateFn") }) } From 1cd34399181bb6f87a6ce0c8b8228693c486b5ff Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 29 Mar 2025 21:38:55 +0100 Subject: [PATCH 03/13] Update README.md fix doc Co-authored-by: nodivbyzero --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffeac2b01..d38e8a89d 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | excluded_without | Excluded Without | | excluded_without_all | Excluded Without All | | unique | Unique | -| isvalid | Verify if the method `Validate() error` does not return an error | +| validateFn | Verify if the method `Validate() error` does not return an error | #### Aliases: From 9c6ec6c0f8c370a6c90976f4ed8cb16c372cea1e Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 17:50:01 +0200 Subject: [PATCH 04/13] improve solution --- README.md | 2 +- _examples/validate_fn/go.mod | 18 +++++++++++ _examples/validate_fn/go.sum | 21 ++++++++++++ _examples/validate_fn/main.go | 52 +++++++++++++++++++++++++++++ baked_in.go | 61 +++++++++++++++++++++++++++-------- doc.go | 11 +++++-- validator_test.go | 34 ++++++++++++++++++- 7 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 _examples/validate_fn/go.mod create mode 100644 _examples/validate_fn/go.sum create mode 100644 _examples/validate_fn/main.go diff --git a/README.md b/README.md index 4376cdba6..28f7e159d 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | excluded_without | Excluded Without | | excluded_without_all | Excluded Without All | | unique | Unique | -| validateFn | Verify if the method `Validate() error` does not return an error | +| validateFn | Verify if the method `Validate() error` does not return an error (or any specified method) | #### Aliases: diff --git a/_examples/validate_fn/go.mod b/_examples/validate_fn/go.mod new file mode 100644 index 000000000..b9077c8ca --- /dev/null +++ b/_examples/validate_fn/go.mod @@ -0,0 +1,18 @@ +module github.com/peczenyj/validator/_examples/validate_fn + +go 1.20 + +replace github.com/go-playground/validator/v10 => ../../../validator + +require github.com/go-playground/validator/v10 v10.26.0 + +require ( + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect +) diff --git a/_examples/validate_fn/go.sum b/_examples/validate_fn/go.sum new file mode 100644 index 000000000..3533f3e00 --- /dev/null +++ b/_examples/validate_fn/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/_examples/validate_fn/main.go b/_examples/validate_fn/main.go new file mode 100644 index 000000000..f45b1fb40 --- /dev/null +++ b/_examples/validate_fn/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/go-playground/validator/v10" +) + +type Enum uint8 + +const ( + Zero Enum = iota + One + Two +) + +func (e Enum) NotZero() bool { + return e != Zero +} + +func (e *Enum) Validate() error { + if e == nil { + return errors.New("can't be nil") + } + + return nil +} + +type Struct struct { + Foo *Enum `validate:"validateFn"` //uses Validate() error by default + Bar Enum `validate:"validateFn=NotZero"` // uses NotZero() bool +} + +func main() { + validate := validator.New() + + var x Struct + + if err := validate.Struct(x); err != nil { + fmt.Printf("Expected Err(s):\n%+v\n", err) + } + + x = Struct{ + Foo: new(Enum), + Bar: One, + } + + if err := validate.Struct(x); err != nil { + fmt.Printf("Unexpected Err(s):\n%+v\n", err) + } +} diff --git a/baked_in.go b/baked_in.go index 2a47a6e08..6e396c206 100644 --- a/baked_in.go +++ b/baked_in.go @@ -2,10 +2,12 @@ package validator import ( "bytes" + "cmp" "context" "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "io/fs" "net" @@ -3049,27 +3051,58 @@ func isEIN(fl FieldLevel) bool { } func isValidateFn(fl FieldLevel) bool { - if instance, ok := tryConvertFieldTo[interface{ Validate() error }](fl.Field()); ok { - return instance.Validate() == nil + const defaultParam = `Validate` + + field := fl.Field() + param := cmp.Or(fl.Param(), defaultParam) + + ok, err := tryCallValidateFn(field, param) + if err != nil { + panic(err) } - return false + return ok } -func tryConvertFieldTo[V any](field reflect.Value) (v V, ok bool) { - v, ok = convertFieldTo[V](field) - if !ok && field.CanAddr() { - v, ok = convertFieldTo[V](field.Addr()) +var ( + errMethodNotFound = errors.New(`method not found`) + errMethodReturnNoValues = errors.New(`method return o values (void)`) + errMethodReturnInvalidType = errors.New(`method should return invalid type`) +) + +func tryCallValidateFn(field reflect.Value, methodName string) (bool, error) { + method := field.MethodByName(methodName) + if !method.IsValid() { + method = field.Addr().MethodByName(methodName) } - return v, ok -} + if !method.IsValid() { + return false, fmt.Errorf("unable to call %q on type %q: %w", + methodName, field.Type().String(), errMethodNotFound) + } -func convertFieldTo[V any](field reflect.Value) (v V, ok bool) { - if v, ok = field.Interface().(V); ok { - return v, ok + returnValues := method.Call([]reflect.Value{}) + if len(returnValues) == 0 { + return false, fmt.Errorf("unable to use result of method %q on type %q: %w", + methodName, field.Type().String(), errMethodReturnNoValues) } - var zero V - return zero, false + firstReturnValue := returnValues[0] + + switch firstReturnValue.Kind() { + case reflect.Bool: + return firstReturnValue.Bool(), nil + case reflect.Interface: + errorType := reflect.TypeOf((*error)(nil)).Elem() + + if firstReturnValue.Type().Implements(errorType) { + return firstReturnValue.IsNil(), nil + } + + return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)", + methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) + default: + return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got %v expect error or bool)", + methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) + } } diff --git a/doc.go b/doc.go index affef3110..91bace77d 100644 --- a/doc.go +++ b/doc.go @@ -758,11 +758,18 @@ in a field of the struct specified via a parameter. # ValidateFn -This validates that an object respects the interface `Validate() error` and -the method `Validate` does not return an error. +This validates that an object responds to a method that can return error or bool. +By default it expects an interface `Validate() error` and check that the method +does not return an error. Other methods can be specified using two signatures: +If the method returns an error, it check if the return value is nil. +If the method returns a boolean, it checks if the value is true. + // to use the default method Validate() error Usage: validateFn + // to use the custom method IsValid() bool (or error) + Usage: validateFn=IsValid + # Alpha Only This validates that a string value contains ASCII alpha characters only diff --git a/validator_test.go b/validator_test.go index 2142ce208..313851818 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14162,7 +14162,11 @@ func (r *NotRed) Validate() error { return nil } -func TestIsValid(t *testing.T) { +func (r NotRed) IsNotRed() bool { + return r.Color != "red" +} + +func TestValidateFn(t *testing.T) { t.Run("using pointer", func(t *testing.T) { validate := New() @@ -14223,4 +14227,32 @@ func TestIsValid(t *testing.T) { Equal(t, fe.Namespace(), "Test2.Inner") Equal(t, fe.Tag(), "validateFn") }) + + t.Run("using struct with custom function", func(t *testing.T) { + validate := New() + + type Test2 struct { + String string + Inner NotRed `validate:"validateFn=IsNotRed"` + } + + var tt2 Test2 + + errs := validate.Struct(&tt2) + Equal(t, errs, nil) + + tt2.Inner = NotRed{Color: "blue"} + + errs = validate.Struct(&tt2) + Equal(t, errs, nil) + + tt2.Inner = NotRed{Color: "red"} + errs = validate.Struct(&tt2) + NotEqual(t, errs, nil) + + fe := errs.(ValidationErrors)[0] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test2.Inner") + Equal(t, fe.Tag(), "validateFn") + }) } From 463e7e3491009f59b87f3f287a1330aa23b3a524 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 17:55:37 +0200 Subject: [PATCH 05/13] fix lint issues --- _examples/validate_fn/main.go | 2 +- validator_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/_examples/validate_fn/main.go b/_examples/validate_fn/main.go index f45b1fb40..bf367bb50 100644 --- a/_examples/validate_fn/main.go +++ b/_examples/validate_fn/main.go @@ -28,7 +28,7 @@ func (e *Enum) Validate() error { } type Struct struct { - Foo *Enum `validate:"validateFn"` //uses Validate() error by default + Foo *Enum `validate:"validateFn"` // uses Validate() error by default Bar Enum `validate:"validateFn=NotZero"` // uses NotZero() bool } diff --git a/validator_test.go b/validator_test.go index 313851818..e83232027 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14197,7 +14197,6 @@ func TestValidateFn(t *testing.T) { Equal(t, fe.Field(), "Inner") Equal(t, fe.Namespace(), "Test.Inner") Equal(t, fe.Tag(), "validateFn") - }) t.Run("using struct", func(t *testing.T) { From 25588691e735ffb57deb1cbbc894526fd1d071bb Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 19:29:10 +0200 Subject: [PATCH 06/13] remove panic --- baked_in.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/baked_in.go b/baked_in.go index 6e396c206..eb7a349fe 100644 --- a/baked_in.go +++ b/baked_in.go @@ -3054,11 +3054,12 @@ func isValidateFn(fl FieldLevel) bool { const defaultParam = `Validate` field := fl.Field() - param := cmp.Or(fl.Param(), defaultParam) + validateFn := cmp.Or(fl.Param(), defaultParam) - ok, err := tryCallValidateFn(field, param) + ok, err := tryCallValidateFn(field, validateFn) if err != nil { - panic(err) + // error can be used in some log + return false } return ok @@ -3070,21 +3071,21 @@ var ( errMethodReturnInvalidType = errors.New(`method should return invalid type`) ) -func tryCallValidateFn(field reflect.Value, methodName string) (bool, error) { - method := field.MethodByName(methodName) +func tryCallValidateFn(field reflect.Value, validateFn string) (bool, error) { + method := field.MethodByName(validateFn) if !method.IsValid() { - method = field.Addr().MethodByName(methodName) + method = field.Addr().MethodByName(validateFn) } if !method.IsValid() { return false, fmt.Errorf("unable to call %q on type %q: %w", - methodName, field.Type().String(), errMethodNotFound) + validateFn, field.Type().String(), errMethodNotFound) } returnValues := method.Call([]reflect.Value{}) if len(returnValues) == 0 { return false, fmt.Errorf("unable to use result of method %q on type %q: %w", - methodName, field.Type().String(), errMethodReturnNoValues) + validateFn, field.Type().String(), errMethodReturnNoValues) } firstReturnValue := returnValues[0] @@ -3100,9 +3101,9 @@ func tryCallValidateFn(field reflect.Value, methodName string) (bool, error) { } return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)", - methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) + validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) default: return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got %v expect error or bool)", - methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) + validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String()) } } From f3be8cc8d128c92512b3078d09b9d61a28943116 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 19:40:59 +0200 Subject: [PATCH 07/13] add some translations (en and pt_BR) --- translations/en/en.go | 5 +++++ translations/en/en_test.go | 9 +++++++++ translations/pt_BR/pt_BR.go | 5 +++++ translations/pt_BR/pt_BR_test.go | 9 +++++++++ 4 files changed, 28 insertions(+) diff --git a/translations/en/en.go b/translations/en/en.go index d0161c73d..9458edc8c 100644 --- a/translations/en/en.go +++ b/translations/en/en.go @@ -1484,6 +1484,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} must be a valid cve identifier", override: false, }, + { + tag: "validateFn", + translation: "{0} must be a valid object", + override: false, + }, } for _, t := range translations { diff --git a/translations/en/en_test.go b/translations/en/en_test.go index 7ae4d002b..44ce09a83 100644 --- a/translations/en/en_test.go +++ b/translations/en/en_test.go @@ -10,6 +10,10 @@ import ( "github.com/go-playground/validator/v10" ) +type Foo struct{} + +func (Foo) IsBar() bool { return false } + func TestTranslations(t *testing.T) { eng := english.New() uni := ut.New(eng, eng) @@ -181,6 +185,7 @@ func TestTranslations(t *testing.T) { CveString string `validate:"cve"` MinDuration time.Duration `validate:"min=1h30m,max=2h"` MaxDuration time.Duration `validate:"min=1h30m,max=2h"` + ValidateFn Foo `validate:"validateFn=IsBar"` } var test Test @@ -805,6 +810,10 @@ func TestTranslations(t *testing.T) { ns: "Test.MaxDuration", expected: "MaxDuration must be 2h or less", }, + { + ns: "Test.ValidateFn", + expected: "ValidateFn must be a valid object", + }, } for _, tt := range tests { diff --git a/translations/pt_BR/pt_BR.go b/translations/pt_BR/pt_BR.go index 0e4384876..27cb2a5ef 100644 --- a/translations/pt_BR/pt_BR.go +++ b/translations/pt_BR/pt_BR.go @@ -1292,6 +1292,11 @@ func RegisterDefaultTranslations(v *validator.Validate, trans ut.Translator) (er translation: "{0} deve ser um identificador cve válido", override: false, }, + { + tag: "validateFn", + translation: "{0} deve ser um objeto válido", + override: false, + }, } for _, t := range translations { diff --git a/translations/pt_BR/pt_BR_test.go b/translations/pt_BR/pt_BR_test.go index aee97d8db..e5880d97c 100644 --- a/translations/pt_BR/pt_BR_test.go +++ b/translations/pt_BR/pt_BR_test.go @@ -10,6 +10,10 @@ import ( "github.com/go-playground/validator/v10" ) +type Foo struct{} + +func (Foo) IsBar() bool { return false } + func TestTranslations(t *testing.T) { ptbr := brazilian_portuguese.New() uni := ut.New(ptbr, ptbr) @@ -142,6 +146,7 @@ func TestTranslations(t *testing.T) { BooleanString string `validate:"boolean"` Image string `validate:"image"` CveString string `validate:"cve"` + ValidateFn Foo `validate:"validateFn=IsBar"` } var test Test @@ -640,6 +645,10 @@ func TestTranslations(t *testing.T) { ns: "Test.CveString", expected: "CveString deve ser um identificador cve válido", }, + { + ns: "Test.ValidateFn", + expected: "ValidateFn deve ser um objeto válido", + }, } for _, tt := range tests { From 3fc893cd57f8a26c19625cbc3f5fcfa1b76aa194 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 19:43:53 +0200 Subject: [PATCH 08/13] fix lint issue funcorder --- validator_instance.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/validator_instance.go b/validator_instance.go index e68a8703c..9362cd731 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -231,23 +231,6 @@ func (v *Validate) RegisterValidationCtx(tag string, fn FuncCtx, callValidationE return v.registerValidation(tag, fn, false, nilCheckable) } -func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool, nilCheckable bool) error { - if len(tag) == 0 { - return errors.New("function Key cannot be empty") - } - - if fn == nil { - return errors.New("function cannot be empty") - } - - _, ok := restrictedTags[tag] - if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) { - panic(fmt.Sprintf(restrictedTagErr, tag)) - } - v.validations[tag] = internalValidationFuncWrapper{fn: fn, runValidationOnNil: nilCheckable} - return nil -} - // RegisterAlias registers a mapping of a single validation tag that // defines a common or complex set of validation(s) to simplify adding validation // to structs. @@ -697,3 +680,20 @@ func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other v.pool.Put(vd) return } + +func (v *Validate) registerValidation(tag string, fn FuncCtx, bakedIn bool, nilCheckable bool) error { + if len(tag) == 0 { + return errors.New("function Key cannot be empty") + } + + if fn == nil { + return errors.New("function cannot be empty") + } + + _, ok := restrictedTags[tag] + if !bakedIn && (ok || strings.ContainsAny(tag, restrictedTagChars)) { + panic(fmt.Sprintf(restrictedTagErr, tag)) + } + v.validations[tag] = internalValidationFuncWrapper{fn: fn, runValidationOnNil: nilCheckable} + return nil +} From f650400d4fad78bde3cb8c6ea8a85de849b2d953 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 20:12:02 +0200 Subject: [PATCH 09/13] improve field verification --- baked_in.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/baked_in.go b/baked_in.go index eb7a349fe..2f4ae0c01 100644 --- a/baked_in.go +++ b/baked_in.go @@ -3058,7 +3058,6 @@ func isValidateFn(fl FieldLevel) bool { ok, err := tryCallValidateFn(field, validateFn) if err != nil { - // error can be used in some log return false } @@ -3073,7 +3072,7 @@ var ( func tryCallValidateFn(field reflect.Value, validateFn string) (bool, error) { method := field.MethodByName(validateFn) - if !method.IsValid() { + if field.CanAddr() && !method.IsValid() { method = field.Addr().MethodByName(validateFn) } From 7b6b95e0aa61e17f5daeb35f6bac5853760e12d9 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 20:12:14 +0200 Subject: [PATCH 10/13] improve example --- _examples/validate_fn/enum_enumer.go | 86 ++++++++++++++++++++++++++++ _examples/validate_fn/main.go | 10 ++-- 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 _examples/validate_fn/enum_enumer.go diff --git a/_examples/validate_fn/enum_enumer.go b/_examples/validate_fn/enum_enumer.go new file mode 100644 index 000000000..15e16b59f --- /dev/null +++ b/_examples/validate_fn/enum_enumer.go @@ -0,0 +1,86 @@ +// Code generated by "enumer -type=Enum"; DO NOT EDIT. + +package main + +import ( + "fmt" + "strings" +) + +const _EnumName = "ZeroOneTwoThree" + +var _EnumIndex = [...]uint8{0, 4, 7, 10, 15} + +const _EnumLowerName = "zeroonetwothree" + +func (i Enum) String() string { + if i >= Enum(len(_EnumIndex)-1) { + return fmt.Sprintf("Enum(%d)", i) + } + return _EnumName[_EnumIndex[i]:_EnumIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _EnumNoOp() { + var x [1]struct{} + _ = x[Zero-(0)] + _ = x[One-(1)] + _ = x[Two-(2)] + _ = x[Three-(3)] +} + +var _EnumValues = []Enum{Zero, One, Two, Three} + +var _EnumNameToValueMap = map[string]Enum{ + _EnumName[0:4]: Zero, + _EnumLowerName[0:4]: Zero, + _EnumName[4:7]: One, + _EnumLowerName[4:7]: One, + _EnumName[7:10]: Two, + _EnumLowerName[7:10]: Two, + _EnumName[10:15]: Three, + _EnumLowerName[10:15]: Three, +} + +var _EnumNames = []string{ + _EnumName[0:4], + _EnumName[4:7], + _EnumName[7:10], + _EnumName[10:15], +} + +// EnumString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func EnumString(s string) (Enum, error) { + if val, ok := _EnumNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _EnumNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Enum values", s) +} + +// EnumValues returns all values of the enum +func EnumValues() []Enum { + return _EnumValues +} + +// EnumStrings returns a slice of all String values of the enum +func EnumStrings() []string { + strs := make([]string, len(_EnumNames)) + copy(strs, _EnumNames) + return strs +} + +// IsAEnum returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Enum) IsAEnum() bool { + for _, v := range _EnumValues { + if i == v { + return true + } + } + return false +} diff --git a/_examples/validate_fn/main.go b/_examples/validate_fn/main.go index bf367bb50..cf61a29b6 100644 --- a/_examples/validate_fn/main.go +++ b/_examples/validate_fn/main.go @@ -7,18 +7,16 @@ import ( "github.com/go-playground/validator/v10" ) +//go:generate enumer -type=Enum type Enum uint8 const ( Zero Enum = iota One Two + Three ) -func (e Enum) NotZero() bool { - return e != Zero -} - func (e *Enum) Validate() error { if e == nil { return errors.New("can't be nil") @@ -29,7 +27,7 @@ func (e *Enum) Validate() error { type Struct struct { Foo *Enum `validate:"validateFn"` // uses Validate() error by default - Bar Enum `validate:"validateFn=NotZero"` // uses NotZero() bool + Bar Enum `validate:"validateFn=IsAEnum"` // uses IsAEnum() bool provided by enumer } func main() { @@ -37,6 +35,8 @@ func main() { var x Struct + x.Bar = Enum(64) + if err := validate.Struct(x); err != nil { fmt.Printf("Expected Err(s):\n%+v\n", err) } From 4343235600441f3dbf41784f3eea52ed4763b345 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 20:24:12 +0200 Subject: [PATCH 11/13] add more unit tests --- validator_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/validator_test.go b/validator_test.go index e83232027..339306775 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14166,6 +14166,10 @@ func (r NotRed) IsNotRed() bool { return r.Color != "red" } +func (r NotRed) DoNothing() {} + +func (r NotRed) String() string { return "not red instance" } + func TestValidateFn(t *testing.T) { t.Run("using pointer", func(t *testing.T) { validate := New() @@ -14254,4 +14258,38 @@ func TestValidateFn(t *testing.T) { Equal(t, fe.Namespace(), "Test2.Inner") Equal(t, fe.Tag(), "validateFn") }) + + t.Run("try validate method with wrong signature or not existent", func(t *testing.T) { + validate := New() + + type Test2 struct { + String string `validate:"validateFn=NotExists"` + Inner NotRed `validate:"validateFn=DoNothing"` + Inner2 NotRed `validate:"validateFn=String"` + } + + var tt2 Test2 + + err := validate.Struct(&tt2) + NotEqual(t, err, nil) + + errs := err.(ValidationErrors) + + Equal(t, len(errs), 3) + + fe := errs[0] + Equal(t, fe.Field(), "String") + Equal(t, fe.Namespace(), "Test2.String") + Equal(t, fe.Tag(), "validateFn") + + fe = errs[1] + Equal(t, fe.Field(), "Inner") + Equal(t, fe.Namespace(), "Test2.Inner") + Equal(t, fe.Tag(), "validateFn") + + fe = errs[2] + Equal(t, fe.Field(), "Inner2") + Equal(t, fe.Namespace(), "Test2.Inner2") + Equal(t, fe.Tag(), "validateFn") + }) } From 3b9058696c2eb352cb3c89acf2927114931d8653 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 20:33:26 +0200 Subject: [PATCH 12/13] add comments --- validator_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/validator_test.go b/validator_test.go index 339306775..513218d54 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14263,9 +14263,9 @@ func TestValidateFn(t *testing.T) { validate := New() type Test2 struct { - String string `validate:"validateFn=NotExists"` - Inner NotRed `validate:"validateFn=DoNothing"` - Inner2 NotRed `validate:"validateFn=String"` + String string `validate:"validateFn=NotExists"` // should fail, method not found + Inner NotRed `validate:"validateFn=DoNothing"` // should fail, return nothing + Inner2 NotRed `validate:"validateFn=String"` // should fail, wrong return (must be error or bool) } var tt2 Test2 From f7f75696f336fb54d3549a4deb80f4639f60e555 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sat, 12 Apr 2025 20:33:36 +0200 Subject: [PATCH 13/13] add benchmarks --- benchmarks_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/benchmarks_test.go b/benchmarks_test.go index ee70f95aa..14de8e695 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -3,6 +3,7 @@ package validator import ( "bytes" sql "database/sql/driver" + "errors" "testing" "time" ) @@ -1097,3 +1098,39 @@ func BenchmarkOneofParallel(b *testing.B) { } }) } + +type T struct{} + +func (*T) Validate() error { return errors.New("ops") } + +func BenchmarkValidateFnSequencial(b *testing.B) { + validate := New() + + type Test struct { + T T `validate:"validateFn"` + } + + test := &Test{} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = validate.Struct(test) + } +} + +func BenchmarkValidateFnParallel(b *testing.B) { + validate := New() + + type Test struct { + T T `validate:"validateFn"` + } + + test := &Test{} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _ = validate.Struct(test) + } + }) +}