From 61d6ab0341413da055b2e09f04dd587d5831f665 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus <48822818+nieomylnieja@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:08:23 +0200 Subject: [PATCH] feat: Add WithNameFunc to Validator (#28) ## Motivation It's often the case that we wan't to dynamically construct the validated entity's name. It would be useful to provide a function that can do that for us. ## Release Notes Added `Validator.WithNameFunc` function which can be used to dynamically set the validator's name per-instance. --- pkg/govy/example_test.go | 19 +++++++++++++++++++ pkg/govy/validator.go | 20 +++++++++++++++++--- pkg/govy/validator_test.go | 14 ++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pkg/govy/example_test.go b/pkg/govy/example_test.go index d5ca8f5..044e814 100644 --- a/pkg/govy/example_test.go +++ b/pkg/govy/example_test.go @@ -72,6 +72,25 @@ func ExampleValidator_WithName() { // - always fails } +// If statically defined name through [govy.Validator.WithName] is not enough, +// you can use [govy.Validator.WithNameFunc]. +// The function receives the entity's instance you're validating and returns a string name. +func ExampleValidator_WithNameFunc() { + v := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + Rules(govy.NewRule(func(name string) error { return fmt.Errorf("always fails") })), + ).WithNameFunc(func(t Teacher) string { return "Teacher " + t.Name }) + + err := v.Validate(Teacher{Name: "John"}) + if err != nil { + fmt.Println(err) + } + + // Output: + // Validation for Teacher John has failed for the following properties: + // - always fails +} + // You can also add [govy.Validator] name during runtime, // by calling [govy.ValidatorError.WithName] function on the returned error. // diff --git a/pkg/govy/validator.go b/pkg/govy/validator.go index 426e776..350b746 100644 --- a/pkg/govy/validator.go +++ b/pkg/govy/validator.go @@ -20,18 +20,28 @@ func New[S any](props ...validationInterface[S]) Validator[S] { // It serves as an aggregator for [PropertyRules]. // Typically, it represents a struct. type Validator[S any] struct { - props []validationInterface[S] - name string + props []validationInterface[S] + name string + nameFunc func(S) string predicateMatcher[S] } // WithName when a rule fails will pass the provided name to [ValidatorError.WithName]. func (v Validator[S]) WithName(name string) Validator[S] { + v.nameFunc = nil v.name = name return v } +// WithNameFunc when a rule fails extracts name from provided function and passes it to [ValidatorError.WithName]. +// The function receives validated entity's instance as an argument. +func (v Validator[S]) WithNameFunc(f func(s S) string) Validator[S] { + v.name = "" + v.nameFunc = f + return v +} + // When accepts predicates which will be evaluated BEFORE [Validator] validates ANY rules. func (v Validator[S]) When(predicate Predicate[S], opts ...WhenOptions) Validator[S] { v.predicateMatcher = v.when(predicate, opts...) @@ -75,7 +85,11 @@ func (v Validator[S]) Validate(st S) error { allErrors = append(allErrors, pErrs...) } if len(allErrors) != 0 { - return NewValidatorError(allErrors).WithName(v.name) + name := v.name + if v.nameFunc != nil { + name = v.nameFunc(st) + } + return NewValidatorError(allErrors).WithName(name) } return nil } diff --git a/pkg/govy/validator_test.go b/pkg/govy/validator_test.go index cb23a7c..8a7d9a8 100644 --- a/pkg/govy/validator_test.go +++ b/pkg/govy/validator_test.go @@ -97,6 +97,20 @@ func TestValidatorWithName(t *testing.T) { - test`) } +func TestValidatorWithNameFunc(t *testing.T) { + r := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). + WithName("test"). + Rules(govy.NewRule(func(v string) error { return errors.New("test") })), + ).WithNameFunc(func(m mockValidatorStruct) string { return "validator with field: " + m.Field }) + + err := r.Validate(mockValidatorStruct{Field: "FIELD"}) + assert.Require(t, assert.Error(t, err)) + assert.EqualError(t, err, `Validation for validator with field: FIELD has failed for the following properties: + - 'test' with value 'test': + - test`) +} + func TestValidatorInferName(t *testing.T) { r := govy.New( govy.For(func(m mockValidatorStruct) string { return "test" }).