Skip to content

Commit 9c6ec6c

Browse files
committed
improve solution
1 parent 21e0136 commit 9c6ec6c

File tree

7 files changed

+181
-18
lines changed

7 files changed

+181
-18
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
262262
| excluded_without | Excluded Without |
263263
| excluded_without_all | Excluded Without All |
264264
| unique | Unique |
265-
| validateFn | Verify if the method `Validate() error` does not return an error |
265+
| validateFn | Verify if the method `Validate() error` does not return an error (or any specified method) |
266266

267267

268268
#### Aliases:

_examples/validate_fn/go.mod

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module github.com/peczenyj/validator/_examples/validate_fn
2+
3+
go 1.20
4+
5+
replace github.com/go-playground/validator/v10 => ../../../validator
6+
7+
require github.com/go-playground/validator/v10 v10.26.0
8+
9+
require (
10+
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
11+
github.com/go-playground/locales v0.14.1 // indirect
12+
github.com/go-playground/universal-translator v0.18.1 // indirect
13+
github.com/leodido/go-urn v1.4.0 // indirect
14+
golang.org/x/crypto v0.33.0 // indirect
15+
golang.org/x/net v0.34.0 // indirect
16+
golang.org/x/sys v0.30.0 // indirect
17+
golang.org/x/text v0.22.0 // indirect
18+
)

_examples/validate_fn/go.sum

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
3+
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
4+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
5+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
6+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
7+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
8+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
9+
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
10+
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
13+
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
14+
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
15+
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
16+
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
17+
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
18+
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
19+
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
20+
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

_examples/validate_fn/main.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/go-playground/validator/v10"
8+
)
9+
10+
type Enum uint8
11+
12+
const (
13+
Zero Enum = iota
14+
One
15+
Two
16+
)
17+
18+
func (e Enum) NotZero() bool {
19+
return e != Zero
20+
}
21+
22+
func (e *Enum) Validate() error {
23+
if e == nil {
24+
return errors.New("can't be nil")
25+
}
26+
27+
return nil
28+
}
29+
30+
type Struct struct {
31+
Foo *Enum `validate:"validateFn"` //uses Validate() error by default
32+
Bar Enum `validate:"validateFn=NotZero"` // uses NotZero() bool
33+
}
34+
35+
func main() {
36+
validate := validator.New()
37+
38+
var x Struct
39+
40+
if err := validate.Struct(x); err != nil {
41+
fmt.Printf("Expected Err(s):\n%+v\n", err)
42+
}
43+
44+
x = Struct{
45+
Foo: new(Enum),
46+
Bar: One,
47+
}
48+
49+
if err := validate.Struct(x); err != nil {
50+
fmt.Printf("Unexpected Err(s):\n%+v\n", err)
51+
}
52+
}

baked_in.go

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package validator
22

33
import (
44
"bytes"
5+
"cmp"
56
"context"
67
"crypto/sha256"
78
"encoding/hex"
89
"encoding/json"
10+
"errors"
911
"fmt"
1012
"io/fs"
1113
"net"
@@ -3049,27 +3051,58 @@ func isEIN(fl FieldLevel) bool {
30493051
}
30503052

30513053
func isValidateFn(fl FieldLevel) bool {
3052-
if instance, ok := tryConvertFieldTo[interface{ Validate() error }](fl.Field()); ok {
3053-
return instance.Validate() == nil
3054+
const defaultParam = `Validate`
3055+
3056+
field := fl.Field()
3057+
param := cmp.Or(fl.Param(), defaultParam)
3058+
3059+
ok, err := tryCallValidateFn(field, param)
3060+
if err != nil {
3061+
panic(err)
30543062
}
30553063

3056-
return false
3064+
return ok
30573065
}
30583066

3059-
func tryConvertFieldTo[V any](field reflect.Value) (v V, ok bool) {
3060-
v, ok = convertFieldTo[V](field)
3061-
if !ok && field.CanAddr() {
3062-
v, ok = convertFieldTo[V](field.Addr())
3067+
var (
3068+
errMethodNotFound = errors.New(`method not found`)
3069+
errMethodReturnNoValues = errors.New(`method return o values (void)`)
3070+
errMethodReturnInvalidType = errors.New(`method should return invalid type`)
3071+
)
3072+
3073+
func tryCallValidateFn(field reflect.Value, methodName string) (bool, error) {
3074+
method := field.MethodByName(methodName)
3075+
if !method.IsValid() {
3076+
method = field.Addr().MethodByName(methodName)
30633077
}
30643078

3065-
return v, ok
3066-
}
3079+
if !method.IsValid() {
3080+
return false, fmt.Errorf("unable to call %q on type %q: %w",
3081+
methodName, field.Type().String(), errMethodNotFound)
3082+
}
30673083

3068-
func convertFieldTo[V any](field reflect.Value) (v V, ok bool) {
3069-
if v, ok = field.Interface().(V); ok {
3070-
return v, ok
3084+
returnValues := method.Call([]reflect.Value{})
3085+
if len(returnValues) == 0 {
3086+
return false, fmt.Errorf("unable to use result of method %q on type %q: %w",
3087+
methodName, field.Type().String(), errMethodReturnNoValues)
30713088
}
30723089

3073-
var zero V
3074-
return zero, false
3090+
firstReturnValue := returnValues[0]
3091+
3092+
switch firstReturnValue.Kind() {
3093+
case reflect.Bool:
3094+
return firstReturnValue.Bool(), nil
3095+
case reflect.Interface:
3096+
errorType := reflect.TypeOf((*error)(nil)).Elem()
3097+
3098+
if firstReturnValue.Type().Implements(errorType) {
3099+
return firstReturnValue.IsNil(), nil
3100+
}
3101+
3102+
return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got interface %v expect error)",
3103+
methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
3104+
default:
3105+
return false, fmt.Errorf("unable to use result of method %q on type %q: %w (got %v expect error or bool)",
3106+
methodName, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
3107+
}
30753108
}

doc.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,11 +758,18 @@ in a field of the struct specified via a parameter.
758758
759759
# ValidateFn
760760
761-
This validates that an object respects the interface `Validate() error` and
762-
the method `Validate` does not return an error.
761+
This validates that an object responds to a method that can return error or bool.
762+
By default it expects an interface `Validate() error` and check that the method
763+
does not return an error. Other methods can be specified using two signatures:
764+
If the method returns an error, it check if the return value is nil.
765+
If the method returns a boolean, it checks if the value is true.
763766
767+
// to use the default method Validate() error
764768
Usage: validateFn
765769
770+
// to use the custom method IsValid() bool (or error)
771+
Usage: validateFn=IsValid
772+
766773
# Alpha Only
767774
768775
This validates that a string value contains ASCII alpha characters only

validator_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14162,7 +14162,11 @@ func (r *NotRed) Validate() error {
1416214162
return nil
1416314163
}
1416414164

14165-
func TestIsValid(t *testing.T) {
14165+
func (r NotRed) IsNotRed() bool {
14166+
return r.Color != "red"
14167+
}
14168+
14169+
func TestValidateFn(t *testing.T) {
1416614170
t.Run("using pointer", func(t *testing.T) {
1416714171
validate := New()
1416814172

@@ -14223,4 +14227,32 @@ func TestIsValid(t *testing.T) {
1422314227
Equal(t, fe.Namespace(), "Test2.Inner")
1422414228
Equal(t, fe.Tag(), "validateFn")
1422514229
})
14230+
14231+
t.Run("using struct with custom function", func(t *testing.T) {
14232+
validate := New()
14233+
14234+
type Test2 struct {
14235+
String string
14236+
Inner NotRed `validate:"validateFn=IsNotRed"`
14237+
}
14238+
14239+
var tt2 Test2
14240+
14241+
errs := validate.Struct(&tt2)
14242+
Equal(t, errs, nil)
14243+
14244+
tt2.Inner = NotRed{Color: "blue"}
14245+
14246+
errs = validate.Struct(&tt2)
14247+
Equal(t, errs, nil)
14248+
14249+
tt2.Inner = NotRed{Color: "red"}
14250+
errs = validate.Struct(&tt2)
14251+
NotEqual(t, errs, nil)
14252+
14253+
fe := errs.(ValidationErrors)[0]
14254+
Equal(t, fe.Field(), "Inner")
14255+
Equal(t, fe.Namespace(), "Test2.Inner")
14256+
Equal(t, fe.Tag(), "validateFn")
14257+
})
1422614258
}

0 commit comments

Comments
 (0)