Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
32 changes: 32 additions & 0 deletions pkg/govy/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions pkg/govy/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
5 changes: 5 additions & 0 deletions pkg/govy/rules_for_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
5 changes: 5 additions & 0 deletions pkg/govy/rules_for_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
1 change: 1 addition & 0 deletions pkg/govy/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type validatorInterface[T any] interface {
type propertyRulesInterface[T any] interface {
validationInterface[T]
cascadeInternal(mode CascadeMode) propertyRulesInterface[T]
getName() string
isPropertyRules()
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/govy/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
nieomylnieja marked this conversation as resolved.
Outdated
filtered = append(filtered, prop)
}
}
v.props = filtered
return v
Comment thread
nieomylnieja marked this conversation as resolved.
}

// 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].
Expand Down
123 changes: 123 additions & 0 deletions pkg/govy/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,129 @@ 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")),
)
err := v.Validate(mockValidatorStruct{Field: "invalid"})
assert.Error(t, err)

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")),
)
err := v.Validate(mockValidatorStruct{Field: "valid"})
assert.Error(t, err)

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")),
)
err := v.Validate(mockValidatorStruct{Field: "anything"})
assert.Error(t, err)

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](0)),
)
err := v.Validate(mockValidatorStruct{Field: "test"})
assert.Error(t, err)

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](0)),
)
err := v.Validate(mockValidatorStruct{Field: "test"})
assert.Error(t, err)

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)
Expand Down
Loading