From 2c7cacaf2f131db51bdec600ac462bd7c7711f01 Mon Sep 17 00:00:00 2001 From: Nikolai Anohyn <82395127+nikolaianohyn@users.noreply.github.com> Date: Sun, 3 Mar 2024 02:33:35 +0700 Subject: [PATCH] Resolving "Validating unexported fields #417" (#1234) https://github.com/go-playground/validator/issues/417 @go-playground/validator-maintainers --------- Co-authored-by: nikolay --- Makefile | 2 +- cache.go | 2 +- options.go | 9 ++++ validator.go | 32 +++++++++++-- validator_instance.go | 29 ++++++------ validator_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index 8bf4f28ab..e097dfaf2 100644 --- a/Makefile +++ b/Makefile @@ -15,4 +15,4 @@ test: bench: $(GOCMD) test -run=NONE -bench=. -benchmem ./... -.PHONY: test lint linters-install \ No newline at end of file +.PHONY: test lint linters-install diff --git a/cache.go b/cache.go index 0f4fa6b5c..b6bdd11a1 100644 --- a/cache.go +++ b/cache.go @@ -126,7 +126,7 @@ func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStr fld = typ.Field(i) - if !fld.Anonymous && len(fld.PkgPath) > 0 { + if !v.privateFieldValidation && !fld.Anonymous && len(fld.PkgPath) > 0 { continue } diff --git a/options.go b/options.go index 1dea56fd7..5e1e40dec 100644 --- a/options.go +++ b/options.go @@ -14,3 +14,12 @@ func WithRequiredStructEnabled() Option { v.requiredStructEnabled = true } } + +// WithPrivateFieldValidation activates validation for unexported fields +// +// It's experimental feature that partially uses unsafe package +func WithPrivateFieldValidation() Option { + return func(v *Validate) { + v.privateFieldValidation = true + } +} diff --git a/validator.go b/validator.go index 8c0b27776..97e2b0e65 100644 --- a/validator.go +++ b/validator.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "strconv" + "unsafe" ) // per validate construct @@ -156,7 +157,7 @@ func (v *validate) traverseField(ctx context.Context, parent reflect.Value, curr structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), - value: current.Interface(), + value: getValue(current), param: ct.param, kind: kind, typ: current.Type(), @@ -416,7 +417,7 @@ OUTER: structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), - value: current.Interface(), + value: getValue(current), param: ct.param, kind: kind, typ: typ, @@ -436,7 +437,7 @@ OUTER: structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), - value: current.Interface(), + value: getValue(current), param: ct.param, kind: kind, typ: typ, @@ -476,7 +477,7 @@ OUTER: structNs: v.str2, fieldLen: uint8(len(cf.altName)), structfieldLen: uint8(len(cf.name)), - value: current.Interface(), + value: getValue(current), param: ct.param, kind: kind, typ: typ, @@ -491,6 +492,29 @@ OUTER: } +func getValue(val reflect.Value) interface{} { + if val.CanInterface() { + return val.Interface() + } + + if val.CanAddr() { + return reflect.NewAt(val.Type(), unsafe.Pointer(val.UnsafeAddr())).Elem().Interface() + } + + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return val.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return val.Uint() + case reflect.Complex64, reflect.Complex128: + return val.Complex() + case reflect.Float32, reflect.Float64: + return val.Float() + default: + return val.String() + } +} + func (v *validate) traverseSlice(ctx context.Context, parent reflect.Value, current reflect.Value, ns []byte, structNs []byte, cf *cField) { var i64 int64 reusableCF := &cField{} diff --git a/validator_instance.go b/validator_instance.go index d5a7be1de..1a345138e 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -80,20 +80,21 @@ type internalValidationFuncWrapper struct { // Validate contains the validator settings and cache type Validate struct { - tagName string - pool *sync.Pool - tagNameFunc TagNameFunc - structLevelFuncs map[reflect.Type]StructLevelFuncCtx - customFuncs map[reflect.Type]CustomTypeFunc - aliases map[string]string - validations map[string]internalValidationFuncWrapper - transTagFunc map[ut.Translator]map[string]TranslationFunc // map[]map[]TranslationFunc - rules map[reflect.Type]map[string]string - tagCache *tagCache - structCache *structCache - hasCustomFuncs bool - hasTagNameFunc bool - requiredStructEnabled bool + tagName string + pool *sync.Pool + tagNameFunc TagNameFunc + structLevelFuncs map[reflect.Type]StructLevelFuncCtx + customFuncs map[reflect.Type]CustomTypeFunc + aliases map[string]string + validations map[string]internalValidationFuncWrapper + transTagFunc map[ut.Translator]map[string]TranslationFunc // map[]map[]TranslationFunc + rules map[reflect.Type]map[string]string + tagCache *tagCache + structCache *structCache + hasCustomFuncs bool + hasTagNameFunc bool + requiredStructEnabled bool + privateFieldValidation bool } // New returns a new instance of 'validate' with sane defaults. diff --git a/validator_test.go b/validator_test.go index 2826ae70e..3b6d26348 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13692,3 +13692,105 @@ func TestOmitNilAndRequired(t *testing.T) { AssertError(t, err2, "OmitNil.Inner.Str", "OmitNil.Inner.Str", "Str", "Str", "required") }) } + +func TestPrivateFieldsStruct(t *testing.T) { + type tc struct { + stct interface{} + errorNum int + } + + tcs := []tc{ + { + stct: &struct { + f1 int8 `validate:"required"` + f2 int16 `validate:"required"` + f3 int32 `validate:"required"` + f4 int64 `validate:"required"` + }{}, + errorNum: 4, + }, + { + stct: &struct { + f1 uint8 `validate:"required"` + f2 uint16 `validate:"required"` + f3 uint32 `validate:"required"` + f4 uint64 `validate:"required"` + }{}, + errorNum: 4, + }, + { + stct: &struct { + f1 complex64 `validate:"required"` + f2 complex128 `validate:"required"` + }{}, + errorNum: 2, + }, + { + stct: &struct { + f1 float32 `validate:"required"` + f2 float64 `validate:"required"` + }{}, + errorNum: 2, + }, + { + stct: struct { + f1 int8 `validate:"required"` + f2 int16 `validate:"required"` + f3 int32 `validate:"required"` + f4 int64 `validate:"required"` + }{}, + errorNum: 4, + }, + { + stct: struct { + f1 uint8 `validate:"required"` + f2 uint16 `validate:"required"` + f3 uint32 `validate:"required"` + f4 uint64 `validate:"required"` + }{}, + errorNum: 4, + }, + { + stct: struct { + f1 complex64 `validate:"required"` + f2 complex128 `validate:"required"` + }{}, + errorNum: 2, + }, + { + stct: struct { + f1 float32 `validate:"required"` + f2 float64 `validate:"required"` + }{}, + errorNum: 2, + }, + { + stct: struct { + f1 *int `validate:"required"` + f2 struct { + f3 int `validate:"required"` + } + }{}, + errorNum: 2, + }, + { + stct: &struct { + f1 *int `validate:"required"` + f2 struct { + f3 int `validate:"required"` + } + }{}, + errorNum: 2, + }, + } + + validate := New(WithPrivateFieldValidation()) + + for _, tc := range tcs { + err := validate.Struct(tc.stct) + NotEqual(t, err, nil) + + errs := err.(ValidationErrors) + Equal(t, len(errs), tc.errorNum) + } +}