Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6d7bb58
feat: update golangci-lint version and add UUID generation utility
nieomylnieja Nov 12, 2025
52869b7
feat: add support for custom property rule identifiers in govy
nieomylnieja Nov 12, 2025
412c2c5
feat: replace name-based property removal with ID-based approach
nieomylnieja Nov 12, 2025
74012be
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Jun 3, 2026
2310196
fix: handle invalid template keys in String
nieomylnieja Jun 16, 2026
946b654
fix: assign ids to pointer and transformed properties
nieomylnieja Jun 16, 2026
3c4d58d
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Jul 8, 2026
789a9e6
Merge branch 'main' into allow-removing-property-rules-by-id
nieomylnieja Jul 20, 2026
2cc2108
fix: preserve unique IDs for derived property rules
nieomylnieja Jul 20, 2026
3f43250
refactor: align infer path internal handling with cascade
nieomylnieja Jul 21, 2026
37a57a4
test: expand property rule ID coverage
nieomylnieja Jul 21, 2026
0142214
perf: reduce validator property rule overhead
nieomylnieja Jul 24, 2026
a03943f
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Jul 25, 2026
dbb9f5f
test: cover property removal plans by ID
nieomylnieja Jul 25, 2026
7f6c431
fix: preserve property IDs and repair benchmarks
nieomylnieja Jul 25, 2026
62d6e39
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Jul 27, 2026
ae36067
fix: use idiomatic property ID accessor
nieomylnieja Jul 27, 2026
1e579fd
test: expand RemovePropertiesByID example coverage
nieomylnieja Aug 7, 2026
b192973
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Aug 7, 2026
597e64e
chore: revert changes
nieomylnieja Aug 7, 2026
4a66897
feat: require explicit property IDs for removal
nieomylnieja Aug 17, 2026
95e0216
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Aug 17, 2026
fde549c
refactor: simplify property removal and builder internals
nieomylnieja Aug 17, 2026
59342fa
Merge remote-tracking branch 'origin/main' into allow-removing-proper…
nieomylnieja Aug 17, 2026
cfd6662
Recursively remove property rules by ID
nieomylnieja Aug 19, 2026
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
21 changes: 20 additions & 1 deletion pkg/govy/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,25 @@ func ExampleValidator_RemovePropertiesByPath() {
// Modified validator passed
}

// RemovePropertiesByID selects direct property rules by IDs set with [govy.PropertyRules.WithID].
// It returns a modified copy and leaves the original validator unchanged.
func ExampleValidator_RemovePropertiesByID() {
ageProperty := govy.For(func(t Teacher) time.Duration { return t.Age }).
WithName("age").
WithID("age").
Rules(rules.GT(time.Duration(0)))
baseValidator := govy.New(ageProperty)
modifiedValidator := baseValidator.RemovePropertiesByID("age")
teacher := Teacher{Age: -1}

fmt.Println(baseValidator.Validate(teacher) != nil)
fmt.Println(modifiedValidator.Validate(teacher))

// Output:
// true
// <nil>
}

// In the interactive tutorial for govy, we've been using
// [govy.PropertyRules.WithName] to provide explicit path segments for our properties.
//
Expand Down Expand Up @@ -2048,7 +2067,7 @@ func ExampleInferPathModeGenerate() {
govyconfig.SetInferredPath(govyconfig.InferredPath{
Path: jsonpath.New().Name("name"),
File: "pkg/govy/example_test.go",
Line: 2055,
Line: 2074,
})

v2 := govy.New(
Expand Down
13 changes: 13 additions & 0 deletions pkg/govy/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func (emptyErr) Error() string { return "" }
// It is the middle-level building block of the validation process,
// aggregated by [Validator] and aggregating [Rule].
type PropertyRules[T, P any] struct {
id string
path jsonpath.Path
pathFunc inferPathFunc
getter internalPropertyGetter[T, P]
Expand Down Expand Up @@ -191,6 +192,14 @@ func (r PropertyRules[T, P]) WithPath(path jsonpath.Path) PropertyRules[T, P] {
return r
}

// WithID sets an identifier for these property rules.
// It can be used with [Validator.RemovePropertiesByID].
// An empty identifier leaves the property without an identifier.
func (r PropertyRules[T, P]) WithID(id string) PropertyRules[T, P] {
r.id = id
return r
}

// WithExamples sets the examples for the property.
func (r PropertyRules[T, P]) WithExamples(examples ...string) PropertyRules[T, P] {
r.examples = append(r.examples, examples...)
Expand Down Expand Up @@ -289,6 +298,10 @@ func (r PropertyRules[T, P]) inferPathModeInternal(mode InferPathMode) PropertyR
return r.InferPath(mode)
}

func (r PropertyRules[T, P]) propertyID() string {
return r.id
}

// plan constructs a validation plan for the property.
func (r PropertyRules[T, P]) plan(builder planBuilder) {
vOpts := newValidationOptions(r.validationOptions...)
Expand Down
10 changes: 10 additions & 0 deletions pkg/govy/rules_for_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ func (r PropertyRulesForMap[M, K, V, P]) WithPath(path jsonpath.Path) PropertyRu
return r
}

// WithID => refer to [PropertyRules.WithID] documentation.
func (r PropertyRulesForMap[M, K, V, P]) WithID(id string) PropertyRulesForMap[M, K, V, P] {
r.mapRules = r.mapRules.WithID(id)
return r
}

// WithExamples => refer to [PropertyRules.WithExamples] documentation.
func (r PropertyRulesForMap[M, K, V, P]) WithExamples(examples ...string) PropertyRulesForMap[M, K, V, P] {
r.mapRules = r.mapRules.WithExamples(examples...)
Expand Down Expand Up @@ -250,6 +256,10 @@ func (r PropertyRulesForMap[M, K, V, P]) inferPathModeInternal(mode InferPathMod
return r.InferPath(mode)
}

func (r PropertyRulesForMap[M, K, V, P]) propertyID() string {
return r.mapRules.propertyID()
}

// plan constructs a validation plan for the property rules.
func (r PropertyRulesForMap[M, K, V, P]) plan(builder planBuilder) {
builder = appendPredicatesToPlanBuilder(builder, r.predicates)
Expand Down
10 changes: 10 additions & 0 deletions pkg/govy/rules_for_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func (r PropertyRulesForSlice[S, T, P]) WithPath(path jsonpath.Path) PropertyRul
return r
}

// WithID => refer to [PropertyRules.WithID] documentation.
func (r PropertyRulesForSlice[S, T, P]) WithID(id string) PropertyRulesForSlice[S, T, P] {
r.sliceRules = r.sliceRules.WithID(id)
return r
}

// WithExamples => refer to [PropertyRules.WithExamples] documentation.
func (r PropertyRulesForSlice[S, T, P]) WithExamples(examples ...string) PropertyRulesForSlice[S, T, P] {
r.sliceRules = r.sliceRules.WithExamples(examples...)
Expand Down Expand Up @@ -164,6 +170,10 @@ func (r PropertyRulesForSlice[S, T, P]) inferPathModeInternal(mode InferPathMode
return r.InferPath(mode)
}

func (r PropertyRulesForSlice[S, T, P]) propertyID() string {
return r.sliceRules.propertyID()
}

// plan generates a validation plan for the property rules.
func (r PropertyRulesForSlice[S, T, P]) plan(builder planBuilder) {
builder = appendPredicatesToPlanBuilder(builder, r.predicates)
Expand Down
1 change: 1 addition & 0 deletions pkg/govy/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type PropertyRulesInterface[T any] interface {
getPath() jsonpath.Path
inferPathModeInternal(mode InferPathMode) PropertyRulesInterface[T]
isPropertyRules()
propertyID() string
}

// RulesInterface defines validation entities on the validation rule level,
Expand Down
22 changes: 22 additions & 0 deletions pkg/govy/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,28 @@ func (v Validator[T]) RemovePropertiesByPath(paths ...jsonpath.Path) Validator[T
return v
}

// RemovePropertiesByID removes every direct property whose identifier matches
// one of the provided identifiers.
// Properties without an identifier and empty identifiers passed to this method are ignored.
// It does not traverse validators included by a property;
// derive the included validator separately to remove one of its properties.
// It returns a modified [Validator] instance without these rules,
// the original [Validator] is not changed.
func (v Validator[T]) RemovePropertiesByID(ids ...string) Validator[T] {
if len(ids) == 0 {
return v
}
filtered := make([]PropertyRulesInterface[T], 0, len(v.props))
for _, prop := range v.props {
id := prop.propertyID()
if id == "" || !slices.Contains(ids, id) {
filtered = append(filtered, prop)
}
}
v.props = filtered
return v
}

// InferPath sets the [InferPathMode] for the validator,
// which controls relative property path inference for validation rules.
func (v Validator[T]) InferPath(mode InferPathMode) Validator[T] {
Expand Down
88 changes: 88 additions & 0 deletions pkg/govy/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,94 @@ func TestValidatorInferPath(t *testing.T) {
})
}

func TestValidatorRemovePropertiesByID(t *testing.T) {
newProperty := func(path, id string) govy.PropertyRules[string, mockValidatorStruct] {
return govy.For(func(mockValidatorStruct) string { return path }).
WithName(path).
WithID(id).
Rules(govy.NewRule(func(string) error { return errors.New(path) }))
}
base := govy.New(
newProperty("first", "remove"),
newProperty("kept", "keep"),
newProperty("second", "remove"),
newProperty("other", "other"),
newProperty("unset", ""),
)

t.Run("removes every matching direct property", func(t *testing.T) {
filtered := base.RemovePropertiesByID("remove", "other", "")
filteredErr := mustValidatorError(t, filtered.Validate(mockValidatorStruct{}))
assert.Require(t, assert.Len(t, filteredErr.Errors, 2))
assert.Equal(t, jsonpath.Parse("kept"), filteredErr.Errors[0].PropertyPath)
assert.Equal(t, jsonpath.Parse("unset"), filteredErr.Errors[1].PropertyPath)

originalErr := mustValidatorError(t, base.Validate(mockValidatorStruct{}))
assert.Len(t, originalErr.Errors, 5)
})

t.Run("ignores empty and unknown IDs", func(t *testing.T) {
emptyIDErr := mustValidatorError(t, base.RemovePropertiesByID("", "missing").Validate(mockValidatorStruct{}))
assert.Len(t, emptyIDErr.Errors, 5)

noIDErr := mustValidatorError(t, base.RemovePropertiesByID().Validate(mockValidatorStruct{}))
assert.Len(t, noIDErr.Errors, 5)
})

t.Run("does not traverse included validators", func(t *testing.T) {
type child struct {
Value string
}
type parent struct {
Child child
}

childValidator := govy.New(
govy.For(func(value child) string { return value.Value }).
WithName("value").
WithID("inner").
Rules(rules.EQ("expected")),
)
parentValidator := govy.New(
govy.For(func(value parent) child { return value.Child }).
WithName("child").
WithID("outer").
Include(childValidator),
)
value := parent{Child: child{Value: "actual"}}

assert.Error(t, parentValidator.RemovePropertiesByID("inner").Validate(value))
assert.NoError(t, parentValidator.RemovePropertiesByID("outer").Validate(value))
})

t.Run("supports scalar, slice, and map properties", func(t *testing.T) {
type value struct {
Scalar string
Slice []string
Map map[string]string
}
validator := govy.New(
govy.For(func(value value) string { return value.Scalar }).
WithName("scalar").
WithID("scalar").
Rules(govy.NewRule(func(string) error { return errors.New("scalar") })),
govy.ForSlice(func(value value) []string { return value.Slice }).
WithName("slice").
WithID("slice").
Rules(govy.NewRule(func([]string) error { return errors.New("slice") })),
govy.ForMap(func(value value) map[string]string { return value.Map }).
WithName("map").
WithID("map").
Rules(govy.NewRule(func(map[string]string) error { return errors.New("map") })),
).
Cascade(govy.CascadeModeContinue).
InferPath(govy.InferPathModeDisable)

assert.Error(t, validator.Validate(value{}))
assert.NoError(t, validator.RemovePropertiesByID("scalar", "slice", "map").Validate(value{}))
})
}

func mustValidatorError(t *testing.T, err error) *govy.ValidatorError {
t.Helper()
return mustErrorType[*govy.ValidatorError](t, err)
Expand Down
Loading