From 23619b4ed2c3f198bf13763504b635f1598bbd93 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 13 Nov 2025 09:25:32 +0100 Subject: [PATCH 1/3] feat: add RemovePropertiesByName method to Validator This commit introduces the `RemovePropertiesByName` method to the `Validator` type, allowing users to remove specific property rules or included validators by their names. The method returns a modified `Validator` instance without altering the original validator. To support this functionality, the following changes were made: - Added a `getName` method to the `propertyRulesInterface` and its implementations (`PropertyRules`, `PropertyRulesForMap`, and `PropertyRulesForSlice`) to retrieve the name of a property. - Updated the `Validator` struct to filter out properties based on the provided names in the `RemovePropertiesByName` method. Comprehensive tests have been added to ensure the correctness of the new method, covering scenarios such as removing single or multiple properties, handling non-existent properties, and verifying that the original validator remains unchanged. --- pkg/govy/example_test.go | 32 +++++++++++ pkg/govy/rules.go | 5 ++ pkg/govy/rules_for_map.go | 5 ++ pkg/govy/rules_for_slice.go | 5 ++ pkg/govy/validation.go | 1 + pkg/govy/validator.go | 26 +++++++++ pkg/govy/validator_test.go | 108 ++++++++++++++++++++++++++++++++++++ 7 files changed, 182 insertions(+) diff --git a/pkg/govy/example_test.go b/pkg/govy/example_test.go index bc4c3e92..03763ec2 100644 --- a/pkg/govy/example_test.go +++ b/pkg/govy/example_test.go @@ -1842,3 +1842,35 @@ func ExamplePlan_validation() { // Output: // predicates without description found at: validator level, $.name } + +// This example demonstrates how to remove specific properties from a [govy.Validator] by their names. +// This is useful when you want to create a modified validator without certain rules. +func ExampleValidator_RemovePropertiesByName() { + baseValidator := govy.New( + govy.For(func(t Teacher) string { return t.Name }). + WithName("name"). + Rules(rules.StringNotEmpty()), + govy.For(func(t Teacher) time.Duration { return t.Age }). + WithName("age"). + Rules(rules.GT(time.Duration(0))), + ) + + teacher := Teacher{Name: "John", Age: -1} + + // Base validator fails because age is negative + err := baseValidator.Validate(teacher) + if err != nil { + fmt.Println("Base validator failed") + } + + // Modified validator passes because age validation is removed + modifiedValidator := baseValidator.RemovePropertiesByName("age") + err = modifiedValidator.Validate(teacher) + if err == nil { + fmt.Println("Modified validator passed") + } + + // Output: + // Base validator failed + // Modified validator passed +} diff --git a/pkg/govy/rules.go b/pkg/govy/rules.go index 0c8d0ef3..9be7f515 100644 --- a/pkg/govy/rules.go +++ b/pkg/govy/rules.go @@ -310,5 +310,10 @@ func newRequiredError() *RuleError { ) } +// getName returns the name of the property. +func (r PropertyRules[T, P]) getName() string { + return r.name +} + // isPropertyRules implements [propertyRulesInterface]. func (r PropertyRules[T, P]) isPropertyRules() {} diff --git a/pkg/govy/rules_for_map.go b/pkg/govy/rules_for_map.go index eecb1caf..ed911ac6 100644 --- a/pkg/govy/rules_for_map.go +++ b/pkg/govy/rules_for_map.go @@ -222,5 +222,10 @@ func (r PropertyRulesForMap[M, K, V, P]) getJSONPathForKey(key any) string { return jsonpath.Join(r.mapRules.name, jsonpath.EscapeSegment(fmt.Sprint(key))) } +// getName returns the name of the property. +func (r PropertyRulesForMap[M, K, V, P]) getName() string { + return r.mapRules.getName() +} + // isPropertyRules implements [propertyRulesInterface]. func (r PropertyRulesForMap[M, K, V, P]) isPropertyRules() {} diff --git a/pkg/govy/rules_for_slice.go b/pkg/govy/rules_for_slice.go index a8f8874f..b87dd284 100644 --- a/pkg/govy/rules_for_slice.go +++ b/pkg/govy/rules_for_slice.go @@ -144,5 +144,10 @@ func (r PropertyRulesForSlice[S, T, P]) getJSONPathForIndex(index int) string { return jsonpath.JoinArray(r.sliceRules.name, jsonpath.NewArrayIndex(index)) } +// getName returns the name of the property. +func (r PropertyRulesForSlice[S, T, P]) getName() string { + return r.sliceRules.getName() +} + // isPropertyRules implements [propertyRulesInterface]. func (r PropertyRulesForSlice[S, T, P]) isPropertyRules() {} diff --git a/pkg/govy/validation.go b/pkg/govy/validation.go index ee3a4c36..b19f1a1f 100644 --- a/pkg/govy/validation.go +++ b/pkg/govy/validation.go @@ -22,6 +22,7 @@ type validatorInterface[T any] interface { type propertyRulesInterface[T any] interface { validationInterface[T] cascadeInternal(mode CascadeMode) propertyRulesInterface[T] + getName() string isPropertyRules() } diff --git a/pkg/govy/validator.go b/pkg/govy/validator.go index fc8ec4eb..1cd2b727 100644 --- a/pkg/govy/validator.go +++ b/pkg/govy/validator.go @@ -71,6 +71,32 @@ func (v Validator[T]) Cascade(mode CascadeMode) Validator[T] { return v } +// RemovePropertiesByName removes any [PropertyRules] or included [Validator] +// which match the provided property names. +// It returns a modified [Validator] instance without these rules, +// the original [Validator] is not changed. +func (v Validator[T]) RemovePropertiesByName(names ...string) Validator[T] { + if len(names) == 0 { + return v + } + filtered := make([]propertyRulesInterface[T], 0, len(v.props)) + for _, prop := range v.props { + propName := prop.getName() + found := false + for _, name := range names { + if propName == name { + found = true + break + } + } + if !found { + filtered = append(filtered, prop) + } + } + v.props = filtered + return v +} + // Validate will first evaluate predicates before validating any rules. // If any predicate does not pass the validation won't be executed (returns nil). // All errors returned by property rules will be aggregated and wrapped in [ValidatorError]. diff --git a/pkg/govy/validator_test.go b/pkg/govy/validator_test.go index f4401d78..178213a2 100644 --- a/pkg/govy/validator_test.go +++ b/pkg/govy/validator_test.go @@ -305,6 +305,114 @@ func TestValidatorCascade(t *testing.T) { } } +func TestValidatorRemovePropertiesByName(t *testing.T) { + t.Run("remove single property by name", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field"). + Rules(rules.EQ("test")), + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("other"). + Rules(rules.EQ("invalid")), + ) + modified := v.RemovePropertiesByName("field") + err := modified.Validate(mockValidatorStruct{Field: "invalid"}) + assert.NoError(t, err) + }) + + t.Run("remove multiple properties by name", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field1"). + Rules(rules.EQ("test")), + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field2"). + Rules(rules.EQ("test")), + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field3"). + Rules(rules.EQ("valid")), + ) + modified := v.RemovePropertiesByName("field1", "field2") + err := modified.Validate(mockValidatorStruct{Field: "valid"}) + assert.NoError(t, err) + }) + + t.Run("remove all properties", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field1"). + Rules(rules.EQ("test")), + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field2"). + Rules(rules.EQ("test")), + ) + modified := v.RemovePropertiesByName("field1", "field2") + err := modified.Validate(mockValidatorStruct{Field: "anything"}) + assert.NoError(t, err) + }) + + t.Run("remove non-existent property", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field"). + Rules(rules.EQ("test")), + ) + modified := v.RemovePropertiesByName("nonexistent") + err := modified.Validate(mockValidatorStruct{Field: "invalid"}) + assert.Error(t, err) + }) + + t.Run("remove with empty names slice", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field"). + Rules(rules.EQ("test")), + ) + modified := v.RemovePropertiesByName() + err := modified.Validate(mockValidatorStruct{Field: "invalid"}) + assert.Error(t, err) + }) + + t.Run("original validator is unchanged", func(t *testing.T) { + original := govy.New( + govy.For(func(m mockValidatorStruct) string { return m.Field }). + WithName("field"). + Rules(rules.EQ("test")), + ) + modified := original.RemovePropertiesByName("field") + + errOriginal := original.Validate(mockValidatorStruct{Field: "invalid"}) + assert.Error(t, errOriginal) + + errModified := modified.Validate(mockValidatorStruct{Field: "invalid"}) + assert.NoError(t, errModified) + }) + + t.Run("remove slice property rules", func(t *testing.T) { + v := govy.New( + govy.ForSlice(func(m mockValidatorStruct) []string { return []string{m.Field} }). + WithName("items"). + Rules(rules.SliceMaxLength[[]string](5)), + ) + modified := v.RemovePropertiesByName("items") + err := modified.Validate(mockValidatorStruct{Field: "test"}) + assert.NoError(t, err) + }) + + t.Run("remove map property rules", func(t *testing.T) { + v := govy.New( + govy.ForMap(func(m mockValidatorStruct) map[string]string { + return map[string]string{"key": m.Field} + }). + WithName("mapping"). + Rules(rules.MapMaxLength[map[string]string](5)), + ) + modified := v.RemovePropertiesByName("mapping") + err := modified.Validate(mockValidatorStruct{Field: "test"}) + assert.NoError(t, err) + }) +} + func mustValidatorError(t *testing.T, err error) *govy.ValidatorError { t.Helper() return mustErrorType[*govy.ValidatorError](t, err) From b0c614d73fbdc10a9831778fd0f1dfc4eda32a0f Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 13 Nov 2025 09:54:36 +0100 Subject: [PATCH 2/3] feat: enhance validator tests with pre-validation error checks The test cases in `TestValidatorRemovePropertiesByName` were updated to include pre-validation error checks before removing properties. This ensures that the validator behaves as expected when validating the initial state of the object. Additionally, the rules for `SliceMaxLength` and `MapMaxLength` were adjusted to use a length of `0` for more stringent testing. --- pkg/govy/validator_test.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/pkg/govy/validator_test.go b/pkg/govy/validator_test.go index 178213a2..ed1a9dbc 100644 --- a/pkg/govy/validator_test.go +++ b/pkg/govy/validator_test.go @@ -315,8 +315,11 @@ func TestValidatorRemovePropertiesByName(t *testing.T) { WithName("other"). Rules(rules.EQ("invalid")), ) + err := v.Validate(mockValidatorStruct{Field: "invalid"}) + assert.Error(t, err) + modified := v.RemovePropertiesByName("field") - err := modified.Validate(mockValidatorStruct{Field: "invalid"}) + err = modified.Validate(mockValidatorStruct{Field: "invalid"}) assert.NoError(t, err) }) @@ -332,8 +335,11 @@ func TestValidatorRemovePropertiesByName(t *testing.T) { WithName("field3"). Rules(rules.EQ("valid")), ) + err := v.Validate(mockValidatorStruct{Field: "valid"}) + assert.Error(t, err) + modified := v.RemovePropertiesByName("field1", "field2") - err := modified.Validate(mockValidatorStruct{Field: "valid"}) + err = modified.Validate(mockValidatorStruct{Field: "valid"}) assert.NoError(t, err) }) @@ -346,8 +352,11 @@ func TestValidatorRemovePropertiesByName(t *testing.T) { WithName("field2"). Rules(rules.EQ("test")), ) + err := v.Validate(mockValidatorStruct{Field: "anything"}) + assert.Error(t, err) + modified := v.RemovePropertiesByName("field1", "field2") - err := modified.Validate(mockValidatorStruct{Field: "anything"}) + err = modified.Validate(mockValidatorStruct{Field: "anything"}) assert.NoError(t, err) }) @@ -392,10 +401,13 @@ func TestValidatorRemovePropertiesByName(t *testing.T) { v := govy.New( govy.ForSlice(func(m mockValidatorStruct) []string { return []string{m.Field} }). WithName("items"). - Rules(rules.SliceMaxLength[[]string](5)), + Rules(rules.SliceMaxLength[[]string](0)), ) + err := v.Validate(mockValidatorStruct{Field: "test"}) + assert.Error(t, err) + modified := v.RemovePropertiesByName("items") - err := modified.Validate(mockValidatorStruct{Field: "test"}) + err = modified.Validate(mockValidatorStruct{Field: "test"}) assert.NoError(t, err) }) @@ -405,10 +417,13 @@ func TestValidatorRemovePropertiesByName(t *testing.T) { return map[string]string{"key": m.Field} }). WithName("mapping"). - Rules(rules.MapMaxLength[map[string]string](5)), + Rules(rules.MapMaxLength[map[string]string](0)), ) + err := v.Validate(mockValidatorStruct{Field: "test"}) + assert.Error(t, err) + modified := v.RemovePropertiesByName("mapping") - err := modified.Validate(mockValidatorStruct{Field: "test"}) + err = modified.Validate(mockValidatorStruct{Field: "test"}) assert.NoError(t, err) }) } From cf1cd1b674493d3bf5467242289ce3fc3a3762da Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Thu, 13 Nov 2025 10:07:55 +0100 Subject: [PATCH 3/3] feat: optimize property filtering in Validator Refactor the `RemovePropertiesByName` method in the `Validator` to use the `slices.Contains` function for checking if a property name exists in the provided list. This change simplifies the logic by replacing the manual loop and boolean flag with a more concise and efficient approach. --- pkg/govy/validator.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pkg/govy/validator.go b/pkg/govy/validator.go index 1cd2b727..2c829f0a 100644 --- a/pkg/govy/validator.go +++ b/pkg/govy/validator.go @@ -2,6 +2,7 @@ package govy import ( "fmt" + "slices" "strings" ) @@ -81,15 +82,7 @@ func (v Validator[T]) RemovePropertiesByName(names ...string) Validator[T] { } filtered := make([]propertyRulesInterface[T], 0, len(v.props)) for _, prop := range v.props { - propName := prop.getName() - found := false - for _, name := range names { - if propName == name { - found = true - break - } - } - if !found { + if !slices.Contains(names, prop.getName()) { filtered = append(filtered, prop) } }