Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ 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 (or any specified method) |


#### Aliases:
| Tag | Description |
Expand Down
86 changes: 86 additions & 0 deletions _examples/validate_fn/enum_enumer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions _examples/validate_fn/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
21 changes: 21 additions & 0 deletions _examples/validate_fn/go.sum
Original file line number Diff line number Diff line change
@@ -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=
52 changes: 52 additions & 0 deletions _examples/validate_fn/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"errors"
"fmt"

"github.com/go-playground/validator/v10"
)

//go:generate enumer -type=Enum
type Enum uint8

const (
Zero Enum = iota
One
Two
Three
)

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=IsAEnum"` // uses IsAEnum() bool provided by enumer
}

func main() {
validate := validator.New()

var x Struct

x.Bar = Enum(64)

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)
}
}
60 changes: 60 additions & 0 deletions baked_in.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package validator

import (
"bytes"
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
Expand Down Expand Up @@ -244,6 +246,7 @@ var (
"cron": isCron,
"spicedb": isSpiceDB,
"ein": isEIN,
"validateFn": isValidateFn,
}
)

Expand Down Expand Up @@ -3046,3 +3049,60 @@ func isEIN(fl FieldLevel) bool {

return einRegex().MatchString(field.String())
}

func isValidateFn(fl FieldLevel) bool {
const defaultParam = `Validate`

field := fl.Field()
validateFn := cmp.Or(fl.Param(), defaultParam)

ok, err := tryCallValidateFn(field, validateFn)
if err != nil {
return false
}

return ok
}

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, validateFn string) (bool, error) {
method := field.MethodByName(validateFn)
if field.CanAddr() && !method.IsValid() {
method = field.Addr().MethodByName(validateFn)
}

if !method.IsValid() {
return false, fmt.Errorf("unable to call %q on type %q: %w",
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",
validateFn, field.Type().String(), errMethodReturnNoValues)
}

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)",
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)",
validateFn, field.Type().String(), errMethodReturnInvalidType, firstReturnValue.Type().String())
}
}
37 changes: 37 additions & 0 deletions benchmarks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package validator
import (
"bytes"
sql "database/sql/driver"
"errors"
"testing"
"time"
)
Expand Down Expand Up @@ -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)
}
})
}
14 changes: 14 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,20 @@ in a field of the struct specified via a parameter.
// For slices of struct:
Usage: unique=field

# ValidateFn

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
Expand Down
5 changes: 5 additions & 0 deletions translations/en/en.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions translations/en/en_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions translations/pt_BR/pt_BR.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading