From 2996074c1b56f56873c561c5276c18e5c90dff6b Mon Sep 17 00:00:00 2001 From: Artem Danilov Date: Wed, 16 Apr 2025 14:29:18 +0200 Subject: [PATCH] feat: implement Valuer interface feature --- util.go | 28 ++++++ validator_test.go | 218 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+) diff --git a/util.go b/util.go index fb7fa788..bdfa886b 100644 --- a/util.go +++ b/util.go @@ -9,6 +9,13 @@ import ( "time" ) +// Valuer is an interface that allows you to expose a method on a type +// (including generic types) that returns a value that is supposed to be validated. +type Valuer interface { + // ValidatorValue returns the value that is supposed to be validated. + ValidatorValue() any +} + // extractTypeInternal gets the actual underlying type of field value. // It will dive into pointers, customTypes and return you the // underlying value and it's kind. @@ -23,6 +30,13 @@ BEGIN: return current, reflect.Ptr, nullable } + if current.CanInterface() { + if v, ok := current.Interface().(Valuer); ok { + current = reflect.ValueOf(v.ValidatorValue()) + goto BEGIN + } + } + current = current.Elem() goto BEGIN @@ -34,6 +48,13 @@ BEGIN: return current, reflect.Interface, nullable } + if current.CanInterface() { + if v, ok := current.Interface().(Valuer); ok { + current = reflect.ValueOf(v.ValidatorValue()) + goto BEGIN + } + } + current = current.Elem() goto BEGIN @@ -42,6 +63,13 @@ BEGIN: default: + if current.CanInterface() { + if v, ok := current.Interface().(Valuer); ok { + current = reflect.ValueOf(v.ValidatorValue()) + goto BEGIN + } + } + if v.v.hasCustomFuncs { if fn, ok := v.v.customFuncs[current.Type()]; ok { current = reflect.ValueOf(fn(current)) diff --git a/validator_test.go b/validator_test.go index 85284ecc..49bb1ee7 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14319,3 +14319,221 @@ func TestValidateFn(t *testing.T) { Equal(t, fe.Tag(), "validateFn") }) } + +type ValuerTypeWithPointerReceiver[T any] struct { + Data T +} + +func (t *ValuerTypeWithPointerReceiver[T]) ValidatorValue() any { + return t.Data +} + +type ValuerTypeWithValueReceiver[T any] struct { + Data T +} + +func (t ValuerTypeWithValueReceiver[T]) ValidatorValue() any { + return t.Data +} + +func TestValuerInterface(t *testing.T) { + t.Run("parent as Valuer (not called)", func(t *testing.T) { + errs := New().Struct(&ValuerTypeWithPointerReceiver[SubTest]{}) + AssertError(t, errs, + "ValuerTypeWithPointerReceiver[github.com/go-playground/validator/v10.SubTest].Data.Test", + "ValuerTypeWithPointerReceiver[github.com/go-playground/validator/v10.SubTest].Data.Test", + "Test", "Test", "required") + }) + t.Run("pointer parent, pointer nested, pointer receiver (called)", func(t *testing.T) { + type Parent struct { + Nested *ValuerTypeWithPointerReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(&Parent{}) + AssertError(t, errs, "Parent.Nested", "Parent.Nested", "Nested", "Nested", "required") + + errs = New().Struct(&Parent{ + Nested: &ValuerTypeWithPointerReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: &ValuerTypeWithPointerReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("pointer parent, pointer nested, value receiver (called)", func(t *testing.T) { + type Parent struct { + Nested *ValuerTypeWithValueReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(&Parent{}) + AssertError(t, errs, "Parent.Nested", "Parent.Nested", "Nested", "Nested", "required") + + errs = New().Struct(&Parent{ + Nested: &ValuerTypeWithValueReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: &ValuerTypeWithValueReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("pointer parent, value nested, pointer receiver (not called)", func(t *testing.T) { + type Parent struct { + Nested ValuerTypeWithPointerReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(&Parent{}) + AssertError(t, errs, "Parent.Nested.Data.Test", "Parent.Nested.Data.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: ValuerTypeWithPointerReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Data.Test", "Parent.Nested.Data.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: ValuerTypeWithPointerReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("pointer parent, value nested, value receiver (called)", func(t *testing.T) { + type Parent struct { + Nested ValuerTypeWithValueReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(&Parent{}) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: ValuerTypeWithValueReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(&Parent{ + Nested: ValuerTypeWithValueReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("value parent, pointer nested, pointer receiver (called)", func(t *testing.T) { + type Parent struct { + Nested *ValuerTypeWithPointerReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(Parent{}) + AssertError(t, errs, "Parent.Nested", "Parent.Nested", "Nested", "Nested", "required") + + errs = New().Struct(Parent{ + Nested: &ValuerTypeWithPointerReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: &ValuerTypeWithPointerReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("value parent, pointer nested, value receiver (called)", func(t *testing.T) { + type Parent struct { + Nested *ValuerTypeWithValueReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(Parent{}) + AssertError(t, errs, "Parent.Nested", "Parent.Nested", "Nested", "Nested", "required") + + errs = New().Struct(Parent{ + Nested: &ValuerTypeWithValueReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: &ValuerTypeWithValueReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("value parent, value nested, pointer receiver (not called)", func(t *testing.T) { + type Parent struct { + Nested ValuerTypeWithPointerReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(Parent{}) + AssertError(t, errs, "Parent.Nested.Data.Test", "Parent.Nested.Data.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: ValuerTypeWithPointerReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Data.Test", "Parent.Nested.Data.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: ValuerTypeWithPointerReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) + t.Run("value parent, value nested, value receiver (called)", func(t *testing.T) { + type Parent struct { + Nested ValuerTypeWithValueReceiver[SubTest] `validate:"required"` + } + + errs := New().Struct(Parent{}) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: ValuerTypeWithValueReceiver[SubTest]{}, + }) + AssertError(t, errs, "Parent.Nested.Test", "Parent.Nested.Test", "Test", "Test", "required") + + errs = New().Struct(Parent{ + Nested: ValuerTypeWithValueReceiver[SubTest]{ + Data: SubTest{ + Test: "Test", + }, + }, + }) + if errs != nil { + t.Fatalf("Expected no error, got: %v", errs) + } + }) +}