diff --git a/internal/uuid/uuid.go b/internal/uuid/uuid.go new file mode 100644 index 00000000..b01a996f --- /dev/null +++ b/internal/uuid/uuid.go @@ -0,0 +1,17 @@ +package uuid + +import ( + "crypto/rand" + "fmt" +) + +func GenerateUUID() string { + b := make([]byte, 16) + _, _ = rand.Read(b) + + // Set version (4) and variant bits + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]) +} diff --git a/internal/uuid/uuid_test.go b/internal/uuid/uuid_test.go new file mode 100644 index 00000000..22066b8f --- /dev/null +++ b/internal/uuid/uuid_test.go @@ -0,0 +1,54 @@ +package uuid + +import ( + "regexp" + "testing" + + "github.com/nobl9/govy/internal/assert" +) + +func TestGenerateUUID(t *testing.T) { + t.Run("generates valid UUID format", func(t *testing.T) { + id := GenerateUUID() + + // UUID v4 format: + pattern := `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$` + matched, err := regexp.MatchString(pattern, id) + assert.NoError(t, err) + assert.True(t, matched) + }) + + t.Run("generates unique IDs", func(t *testing.T) { + ids := make(map[string]bool) + iterations := 1000 + + for range iterations { + id := GenerateUUID() + + assert.False(t, ids[id]) + ids[id] = true + } + + assert.Equal(t, iterations, len(ids)) + }) + + t.Run("has correct length", func(t *testing.T) { + id := GenerateUUID() + + expectedLen := 36 // 32 hex chars + 4 hyphens + assert.Equal(t, expectedLen, len(id)) + }) + + t.Run("has correct version bits", func(t *testing.T) { + id := GenerateUUID() + // Version should be 4 (at position 14) + assert.Equal(t, byte('4'), id[14]) + }) + + t.Run("has correct variant bits", func(t *testing.T) { + id := GenerateUUID() + // Variant should be 8, 9, a, or b (at position 19) + variant := id[19] + assert.True(t, variant == '8' || variant == '9' || variant == 'a' || variant == 'b') + }) +} diff --git a/pkg/govy/identifier.go b/pkg/govy/identifier.go new file mode 100644 index 00000000..94a46ff8 --- /dev/null +++ b/pkg/govy/identifier.go @@ -0,0 +1,27 @@ +package govy + +import "github.com/nobl9/govy/internal/uuid" + +// instanceID is a composite identifier used to identify [Validator] and [PropertyRules] variations. +type instanceID struct { + // generatedID is always filled and generated upon creation of [instanceID]. + generatedID string + // userSuppliedID overrides generatedID and is supplied by the user. + userSuppliedID string +} + +func newInstanceID() instanceID { + return instanceID{generatedID: uuid.GenerateUUID()} +} + +func (i instanceID) WithUserSuppliedID(id string) instanceID { + i.userSuppliedID = id + return i +} + +func (i instanceID) GetID() string { + if i.userSuppliedID != "" { + return i.userSuppliedID + } + return i.generatedID +} diff --git a/pkg/govy/rules.go b/pkg/govy/rules.go index d6f6115a..e3539705 100644 --- a/pkg/govy/rules.go +++ b/pkg/govy/rules.go @@ -20,6 +20,7 @@ func For[T, P any](getter PropertyGetter[T, P]) PropertyRules[T, P] { // validation will not proceed. func ForPointer[T, P any](getter PropertyGetter[*T, P]) PropertyRules[T, P] { return PropertyRules[T, P]{ + id: newInstanceID(), pathFunc: getInferPathFunc(getCallersAndProgramCounter(4)), getter: func(parent P) (indirect T, err error) { ptr := getter(parent) @@ -40,6 +41,7 @@ func ForPointer[T, P any](getter PropertyGetter[*T, P]) PropertyRules[T, P] { func Transform[T, N, P any](getter PropertyGetter[T, P], transform Transformer[T, N]) PropertyRules[N, P] { typInfo := typeinfo.Get[T]() return PropertyRules[N, P]{ + id: newInstanceID(), pathFunc: getInferPathFunc(getCallersAndProgramCounter(4)), transformGetter: func(parent P) (transformed N, original any, err error) { v := getter(parent) @@ -60,6 +62,7 @@ func Transform[T, N, P any](getter PropertyGetter[T, P], transform Transformer[T // It wraps the getter function in [internalPropertyGetter] and adds path inference. func forConstructor[T, P any](getter PropertyGetter[T, P]) PropertyRules[T, P] { return PropertyRules[T, P]{ + id: newInstanceID(), pathFunc: getInferPathFunc(getCallersAndProgramCounter(4)), getter: func(parent P) (v T, err error) { return getter(parent), nil }, } @@ -69,6 +72,7 @@ func forConstructor[T, P any](getter PropertyGetter[T, P]) PropertyRules[T, P] { // Used for internal rules in [ForSlice] and [ForMap] where paths are managed separately. func forConstructorWithoutPathInference[T, P any](getter PropertyGetter[T, P]) PropertyRules[T, P] { return PropertyRules[T, P]{ + id: newInstanceID(), getter: func(parent P) (v T, err error) { return getter(parent), nil }, } } @@ -98,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 instanceID path jsonpath.Path pathFunc inferPathFunc getter internalPropertyGetter[T, P] @@ -186,6 +191,13 @@ func (r PropertyRules[T, P]) WithPath(path jsonpath.Path) PropertyRules[T, P] { return r } +// WithID sets a unique identifier for these property rules. +// The identifier can be used with [Validator.RemovePropertiesByID]. +func (r PropertyRules[T, P]) WithID(id string) PropertyRules[T, P] { + r.id = r.id.WithUserSuppliedID(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...) @@ -253,6 +265,11 @@ func (r PropertyRules[T, P]) InferPath(mode InferPathMode) PropertyRules[T, P] { return r } +// GetID returns the identifier for these property rules. +func (r PropertyRules[T, P]) GetID() string { + return r.id.GetID() +} + // cascadeInternal is an internal wrapper around [PropertyRules.Cascade] which // fulfills [PropertyRulesInterface] interface. // If the [CascadeMode] is already set, it won't change it. diff --git a/pkg/govy/rules_for_map.go b/pkg/govy/rules_for_map.go index 36faa666..cba7df6a 100644 --- a/pkg/govy/rules_for_map.go +++ b/pkg/govy/rules_for_map.go @@ -109,6 +109,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...) @@ -206,6 +212,11 @@ func (r PropertyRulesForMap[M, K, V, P]) InferPath(mode InferPathMode) PropertyR return r } +// GetID => refer to [PropertyRules.GetID] documentation. +func (r PropertyRulesForMap[M, K, V, P]) GetID() string { + return r.mapRules.GetID() +} + // cascadeInternal is an internal wrapper around [PropertyRulesForMap.Cascade] which // fulfills [PropertyRulesInterface] interface. // If the [CascadeMode] is already set, it won't change it. diff --git a/pkg/govy/rules_for_slice.go b/pkg/govy/rules_for_slice.go index e0ad8c3a..a31cdb2c 100644 --- a/pkg/govy/rules_for_slice.go +++ b/pkg/govy/rules_for_slice.go @@ -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...) @@ -133,6 +139,11 @@ func (r PropertyRulesForSlice[S, T, P]) InferPath(mode InferPathMode) PropertyRu return r } +// GetID => refer to [PropertyRules.GetID] documentation. +func (r PropertyRulesForSlice[S, T, P]) GetID() string { + return r.sliceRules.GetID() +} + // cascadeInternal is an internal wrapper around [PropertyRulesForSlice.Cascade] which // fulfills [PropertyRulesInterface] interface. // If the [CascadeMode] is already set, it won't change it. diff --git a/pkg/govy/rules_test.go b/pkg/govy/rules_test.go index 6a38fc64..aceebce2 100644 --- a/pkg/govy/rules_test.go +++ b/pkg/govy/rules_test.go @@ -463,6 +463,50 @@ type mockStruct struct { Field string `json:"field"` } +func TestPropertyRulesWithID(t *testing.T) { + t.Run("property rules", func(t *testing.T) { + prop := govy.For(func(m mockStruct) string { return m.Field }). + WithName("field"). + WithID("custom-field-id"). + Rules(rules.EQ("test")) + + assert.Equal(t, "custom-field-id", prop.GetID()) + }) + + t.Run("slice property rules", func(t *testing.T) { + prop := govy.ForSlice(func(m mockStruct) []string { return []string{m.Field} }). + WithName("items"). + WithID("custom-slice-id"). + Rules(rules.SliceMaxLength[[]string](10)) + + assert.Equal(t, "custom-slice-id", prop.GetID()) + }) + + t.Run("map property rules", func(t *testing.T) { + prop := govy.ForMap(func(m mockStruct) map[string]string { + return map[string]string{"key": m.Field} + }). + WithName("data"). + WithID("custom-map-id"). + Rules(rules.MapMaxLength[map[string]string](10)) + + assert.Equal(t, "custom-map-id", prop.GetID()) + }) + + t.Run("WithID creates a copy", func(t *testing.T) { + original := govy.For(func(m mockStruct) string { return m.Field }). + WithName("field"). + Rules(rules.EQ("test")) + + originalID := original.GetID() + modified := original.WithID("custom-id") + + assert.Equal(t, originalID, original.GetID()) + assert.Equal(t, "custom-id", modified.GetID()) + assert.True(t, originalID != modified.GetID()) + }) +} + func BenchmarkFor(b *testing.B) { for b.Loop() { _ = govy.For(func(m mockStruct) string { return m.Field }) diff --git a/pkg/govy/validation.go b/pkg/govy/validation.go index a9e5aecd..ab50f035 100644 --- a/pkg/govy/validation.go +++ b/pkg/govy/validation.go @@ -18,6 +18,7 @@ type ValidatorInterface[T any] interface { type PropertyRulesInterface[T any] interface { validationInterface[T] cascadeInternal(mode CascadeMode) PropertyRulesInterface[T] + GetID() string getPath() jsonpath.Path inferPathModeInternal(mode InferPathMode) PropertyRulesInterface[T] isPropertyRules() diff --git a/pkg/govy/validator.go b/pkg/govy/validator.go index 74d15849..2f8017af 100644 --- a/pkg/govy/validator.go +++ b/pkg/govy/validator.go @@ -8,13 +8,17 @@ import ( // New creates a new [Validator] aggregating the provided property rules. func New[T any](props ...PropertyRulesInterface[T]) Validator[T] { - return Validator[T]{props: props} + return Validator[T]{ + id: newInstanceID(), + props: props, + } } // Validator is the top level validation entity. // It serves as an aggregator for [PropertyRules]. // Typically, it represents a struct. type Validator[T any] struct { + id instanceID props []PropertyRulesInterface[T] name string nameFunc func(value T) string @@ -30,6 +34,12 @@ func (v Validator[T]) WithName(name string) Validator[T] { return v } +// WithID sets a unique identifier for this validator. +func (v Validator[T]) WithID(id string) Validator[T] { + v.id = v.id.WithUserSuppliedID(id) + 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[T]) WithNameFunc(f func(value T) string) Validator[T] { @@ -75,6 +85,23 @@ func (v Validator[T]) RemovePropertiesByPath(paths ...jsonpath.Path) Validator[T return v } +// RemovePropertiesByID removes any [PropertyRules] matching the provided identifiers. +// 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 { + if !slices.Contains(ids, prop.GetID()) { + 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] { @@ -86,6 +113,11 @@ func (v Validator[T]) InferPath(mode InferPathMode) Validator[T] { return v } +// GetID returns the identifier for this validator. +func (v Validator[T]) GetID() string { + return v.id.GetID() +} + // 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 e79af4c8..4c93acc0 100644 --- a/pkg/govy/validator_test.go +++ b/pkg/govy/validator_test.go @@ -612,6 +612,279 @@ func TestValidatorInferPath(t *testing.T) { }) } +func TestValidatorWithID(t *testing.T) { + t.Run("set custom ID", func(t *testing.T) { + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). + WithName("test"). + Rules(govy.NewRule(func(v string) error { return nil })), + ).WithID("custom-validator-id") + + err := v.Validate(mockValidatorStruct{}) + assert.NoError(t, err) + }) + + t.Run("WithID creates a copy", func(t *testing.T) { + original := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test" }). + WithName("test"). + Rules(govy.NewRule(func(v string) error { return nil })), + ) + modified := original.WithID("custom-id") + + assert.NoError(t, original.Validate(mockValidatorStruct{})) + assert.NoError(t, modified.Validate(mockValidatorStruct{})) + }) +} + +func TestValidatorRemovePropertiesByID(t *testing.T) { + t.Run("remove single property by ID", func(t *testing.T) { + err1 := errors.New("error1") + err2 := errors.New("error2") + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })) + prop2 := govy.For(func(m mockValidatorStruct) string { return "test2" }). + WithName("property2"). + Rules(govy.NewRule(func(v string) error { return err2 })) + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID()) + err := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("property2"), err.Errors[0].PropertyPath) + }) + + t.Run("remove multiple properties by ID", func(t *testing.T) { + err1 := errors.New("error1") + err2 := errors.New("error2") + err3 := errors.New("error3") + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })) + prop2 := govy.For(func(m mockValidatorStruct) string { return "test2" }). + WithName("property2"). + Rules(govy.NewRule(func(v string) error { return err2 })) + prop3 := govy.For(func(m mockValidatorStruct) string { return "test3" }). + WithName("property3"). + Rules(govy.NewRule(func(v string) error { return err3 })) + v := govy.New(prop1, prop2, prop3) + + filteredV := v.RemovePropertiesByID(prop1.GetID(), prop3.GetID()) + err := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("property2"), err.Errors[0].PropertyPath) + }) + + t.Run("remove property by generated ID", func(t *testing.T) { + err1 := errors.New("error1") + err2 := errors.New("error2") + + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })) + + prop2 := govy.For(func(m mockValidatorStruct) string { return "test2" }). + WithName("property2"). + Rules(govy.NewRule(func(v string) error { return err2 })) + + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID()) + err := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("property2"), err.Errors[0].PropertyPath) + }) + + t.Run("remove pointer property by generated ID", func(t *testing.T) { + type withPointers struct { + Primary *string + Secondary *string + } + + prop1 := govy.ForPointer(func(w withPointers) *string { return w.Primary }). + WithName("primary"). + Rules(rules.StringMinLength(3)) + prop2 := govy.ForPointer(func(w withPointers) *string { return w.Secondary }). + WithName("secondary"). + Rules(rules.StringMinLength(3)) + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID()) + err := mustValidatorError(t, filteredV.Validate(withPointers{ + Primary: ptr("a"), + Secondary: ptr("b"), + })) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("secondary"), err.Errors[0].PropertyPath) + }) + + t.Run("remove transformed property by generated ID", func(t *testing.T) { + type withTransformed struct { + Primary string + Secondary string + } + transform := func(s string) (int, error) { return len(s), nil } + + prop1 := govy.Transform(func(w withTransformed) string { return w.Primary }, transform). + WithName("primary"). + Rules(rules.GT(1)) + prop2 := govy.Transform(func(w withTransformed) string { return w.Secondary }, transform). + WithName("secondary"). + Rules(rules.GT(1)) + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID()) + err := mustValidatorError(t, filteredV.Validate(withTransformed{ + Primary: "a", + Secondary: "b", + })) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("secondary"), err.Errors[0].PropertyPath) + }) + + t.Run("remove all properties", func(t *testing.T) { + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return errors.New("error1") })) + prop2 := govy.For(func(m mockValidatorStruct) string { return "test2" }). + WithName("property2"). + Rules(govy.NewRule(func(v string) error { return errors.New("error2") })) + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID(), prop2.GetID()) + err := filteredV.Validate(mockValidatorStruct{}) + + assert.NoError(t, err) + }) + + t.Run("remove non-existent property", func(t *testing.T) { + err1 := errors.New("error1") + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })) + v := govy.New(prop1) + + filteredV := v.RemovePropertiesByID("non-existent-uuid-12345") + err := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("property1"), err.Errors[0].PropertyPath) + }) + + t.Run("remove with empty IDs slice", func(t *testing.T) { + err1 := errors.New("error1") + v := govy.New( + govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })), + ) + + filteredV := v.RemovePropertiesByID() + err := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + + assert.Require(t, assert.Len(t, err.Errors, 1)) + assert.Equal(t, jsonpath.Parse("property1"), err.Errors[0].PropertyPath) + }) + + t.Run("original validator is unchanged", func(t *testing.T) { + err1 := errors.New("error1") + err2 := errors.New("error2") + prop1 := govy.For(func(m mockValidatorStruct) string { return "test1" }). + WithName("property1"). + Rules(govy.NewRule(func(v string) error { return err1 })) + prop2 := govy.For(func(m mockValidatorStruct) string { return "test2" }). + WithName("property2"). + Rules(govy.NewRule(func(v string) error { return err2 })) + v := govy.New(prop1, prop2) + + filteredV := v.RemovePropertiesByID(prop1.GetID()) + + originalErr := mustValidatorError(t, v.Validate(mockValidatorStruct{})) + assert.Len(t, originalErr.Errors, 2) + + filteredErr := mustValidatorError(t, filteredV.Validate(mockValidatorStruct{})) + assert.Len(t, filteredErr.Errors, 1) + }) + + t.Run("remove nested validators with include", func(t *testing.T) { + type nested struct { + Value string + } + type parent struct { + Nested nested + } + + nestedValidator := govy.New( + govy.For(func(n nested) string { return n.Value }). + WithName("value"). + Rules(rules.EQ("expected")), + ).WithID("nested-validator") + + nestedProp := govy.For(func(p parent) nested { return p.Nested }). + WithName("nested"). + Include(nestedValidator) + + parentValidator := govy.New(nestedProp) + + obj := parent{Nested: nested{Value: "wrong"}} + + err := mustValidatorError(t, parentValidator.Validate(obj)) + assert.Require(t, assert.Len(t, err.Errors, 1)) + + filteredValidator := parentValidator.RemovePropertiesByID(nestedProp.GetID()) + filteredErr := filteredValidator.Validate(obj) + assert.NoError(t, filteredErr) + }) + + t.Run("remove slice property rules", func(t *testing.T) { + type withSlice struct { + Items []string + } + + sliceProp := govy.ForSlice(func(w withSlice) []string { return w.Items }). + WithName("items"). + Rules(rules.SliceMaxLength[[]string](1)) + + v := govy.New(sliceProp) + + obj := withSlice{Items: []string{"a", "b"}} + + err := mustValidatorError(t, v.Validate(obj)) + assert.Require(t, assert.Len(t, err.Errors, 1)) + + filteredV := v.RemovePropertiesByID(sliceProp.GetID()) + filteredErr := filteredV.Validate(obj) + assert.NoError(t, filteredErr) + }) + + t.Run("remove map property rules", func(t *testing.T) { + type withMap struct { + Data map[string]string + } + + mapProp := govy.ForMap(func(w withMap) map[string]string { return w.Data }). + WithName("data"). + Rules(rules.MapMaxLength[map[string]string](1)) + + v := govy.New(mapProp) + + obj := withMap{Data: map[string]string{"a": "1", "b": "2"}} + + err := mustValidatorError(t, v.Validate(obj)) + assert.Require(t, assert.Len(t, err.Errors, 1)) + + filteredV := v.RemovePropertiesByID(mapProp.GetID()) + filteredErr := filteredV.Validate(obj) + assert.NoError(t, filteredErr) + }) +} + func mustValidatorError(t *testing.T, err error) *govy.ValidatorError { t.Helper() return mustErrorType[*govy.ValidatorError](t, err) diff --git a/pkg/rules/rules_test.go b/pkg/rules/rules_test.go index b872c0d3..c6deb717 100644 --- a/pkg/rules/rules_test.go +++ b/pkg/rules/rules_test.go @@ -34,8 +34,8 @@ func TestRules_EnsureTestsAndBenchmarksAreWritten(t *testing.T) { t.Fatalf("Failed to read directory: %v", err) } - var files []*ast.File - var fileNames []string + files := make([]*ast.File, 0, len(entries)) + fileNames := make([]string, 0, len(entries)) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { continue diff --git a/pkg/rules/string_test.go b/pkg/rules/string_test.go index 5353dba0..4150532d 100644 --- a/pkg/rules/string_test.go +++ b/pkg/rules/string_test.go @@ -40,7 +40,7 @@ func TestStringNotEmpty(t *testing.T) { func BenchmarkStringNotEmpty(b *testing.B) { for _, tc := range stringNotEmptyTestCases { rule := StringNotEmpty() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -77,7 +77,7 @@ func TestStringMatchRegexp(t *testing.T) { func BenchmarkStringMatchRegexp(b *testing.B) { for _, tc := range stringMatchRegexpTestCases { rule := StringMatchRegexp(stringMatchRegexpRegexp) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -114,7 +114,7 @@ func TestStringDenyRegexp(t *testing.T) { func BenchmarkStringDenyRegexp(b *testing.B) { for _, tc := range stringDenyRegexpTestCases { rule := StringDenyRegexp(stringDenyRegexpRegexp) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -157,7 +157,7 @@ func TestStringDNSLabel(t *testing.T) { func BenchmarkStringDNSLabel(b *testing.B) { for _, tc := range stringDNSLabelTestCases { rule := StringDNSLabel() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -221,7 +221,7 @@ func TestStringDNSSubdomain(t *testing.T) { func BenchmarkStringDNSSubdomain(b *testing.B) { for _, tc := range stringDNSSubdomainTestCases { rule := StringDNSSubdomain() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -261,7 +261,7 @@ func TestStringASCII(t *testing.T) { func BenchmarkStringASCII(b *testing.B) { for _, tc := range stringASCIITestCases { rule := StringASCII() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -305,7 +305,7 @@ func TestStringUUID(t *testing.T) { func BenchmarkStringUUID(b *testing.B) { for _, tc := range stringUUIDTestCases { rule := StringUUID() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -350,7 +350,7 @@ func TestStringEmail(t *testing.T) { func BenchmarkStringEmail(b *testing.B) { for _, tc := range stringEmailTestCases { rule := StringEmail() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -380,7 +380,7 @@ func TestStringURL(t *testing.T) { func BenchmarkStringURL(b *testing.B) { for _, tc := range urlTestCases { rule := StringURL() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.url) } } @@ -416,7 +416,7 @@ func TestStringMAC(t *testing.T) { func BenchmarkStringMAC(b *testing.B) { for _, tc := range stringMACTestCases { rule := StringMAC() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -456,7 +456,7 @@ func TestStringIP(t *testing.T) { func BenchmarkStringIP(b *testing.B) { for _, tc := range stringIPTestCases { rule := StringIP() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -495,7 +495,7 @@ func TestStringIPv4(t *testing.T) { func BenchmarkStringIPv4(b *testing.B) { for _, tc := range stringIPv4TestCases { rule := StringIPv4() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -534,7 +534,7 @@ func TestStringIPv6(t *testing.T) { func BenchmarkStringIPv6(b *testing.B) { for _, tc := range stringIPv6TestCases { rule := StringIPv6() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -576,7 +576,7 @@ func TestStringCIDR(t *testing.T) { func BenchmarkStringCIDR(b *testing.B) { for _, tc := range stringCIDRTestCases { rule := StringCIDR() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -625,7 +625,7 @@ func TestStringCIDRv4(t *testing.T) { func BenchmarkStringCIDRv4(b *testing.B) { for _, tc := range stringCIDRv4TestCases { rule := StringCIDRv4() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -667,7 +667,7 @@ func TestStringCIDRv6(t *testing.T) { func BenchmarkStringCIDRv6(b *testing.B) { for _, tc := range stringCIDRv6TestCases { rule := StringCIDRv6() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -700,7 +700,7 @@ func TestStringJSON(t *testing.T) { func BenchmarkStringJSON(b *testing.B) { for _, tc := range stringJSONTestCases { rule := StringJSON() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -759,7 +759,7 @@ func TestStringContains(t *testing.T) { func BenchmarkStringContains(b *testing.B) { for _, tc := range stringContainsTestCases { rule := StringContains(tc.substrings...) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -819,7 +819,7 @@ func TestStringExcludes(t *testing.T) { func BenchmarkStringExcludes(b *testing.B) { for _, tc := range stringExcludesTestCases { rule := StringExcludes(tc.substrings...) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -865,7 +865,7 @@ func TestStringStartsWith(t *testing.T) { func BenchmarkStringStartsWith(b *testing.B) { for _, tc := range stringStartsWithTestCases { rule := StringStartsWith(tc.prefixes...) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -911,7 +911,7 @@ func TestStringEndsWith(t *testing.T) { func BenchmarkStringEndsWith(b *testing.B) { for _, tc := range stringEndsWithTestCases { rule := StringEndsWith(tc.suffixes...) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -954,7 +954,7 @@ func TestStringTitle(t *testing.T) { func BenchmarkStringTitle(b *testing.B) { for _, tc := range stringTitleTestCases { rule := StringTitle() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1041,7 +1041,7 @@ func TestStringGitRef(t *testing.T) { func BenchmarkStringGitRef(b *testing.B) { for _, tc := range stringGitRefTestCases { rule := StringGitRef() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1123,7 +1123,7 @@ func BenchmarkStringFileSystemPath(b *testing.B) { testCases := getStringFileSystemPathTestCases(root) for _, tc := range testCases { rule := StringFileSystemPath() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1174,7 +1174,7 @@ func BenchmarkStringFilePath(b *testing.B) { testCases := getStringFilePathTestCases(root) for _, tc := range testCases { rule := StringFilePath() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1225,7 +1225,7 @@ func BenchmarkStringDirPath(b *testing.B) { testCases := getStringDirPathTestCases(root) for _, tc := range testCases { rule := StringDirPath() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1314,7 +1314,7 @@ func TestStringMatchFileSystemPath(t *testing.T) { func BenchmarkStringMatchFileSystemPath(b *testing.B) { for _, tc := range stringMatchFileSystemPathTestCases { rule := StringMatchFileSystemPath(tc.pattern) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1373,7 +1373,7 @@ func TestStringRegexp(t *testing.T) { func BenchmarkStringRegexp(b *testing.B) { for _, tc := range stringRegexpTestCases { rule := StringRegexp() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1511,7 +1511,7 @@ func TestStringCrontab(t *testing.T) { } func BenchmarkStringCrontab(b *testing.B) { - for range b.N { + for b.Loop() { testCases := getStringCronTestCases() for _, tc := range testCases { _ = StringCrontab().Validate(tc.in) @@ -1563,7 +1563,7 @@ func TestStringDateTime(t *testing.T) { func BenchmarkStringDateTime(b *testing.B) { for _, tc := range stringDateTimeTestCases { rule := StringDateTime(tc.layout) - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1600,7 +1600,7 @@ func TestStringTimeZone(t *testing.T) { func BenchmarkStringTimeZone(b *testing.B) { for _, tc := range stringDateTimeTestCases { rule := StringTimeZone() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1639,7 +1639,7 @@ func TestStringAlpha(t *testing.T) { func BenchmarkStringAlpha(b *testing.B) { for _, tc := range stringAlphaTestCases { rule := StringAlpha() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1682,7 +1682,7 @@ func TestStringAlphanumeric(t *testing.T) { func BenchmarkStringAlphanumeric(b *testing.B) { for _, tc := range stringAlphanumericTestCases { rule := StringAlphanumeric() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1725,7 +1725,7 @@ func TestStringAlphaUnicode(t *testing.T) { func BenchmarkStringAlphaUnicode(b *testing.B) { for _, tc := range stringAlphaUnicodeTestCases { rule := StringAlphaUnicode() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1773,7 +1773,7 @@ func TestStringAlphanumericUnicode(t *testing.T) { func BenchmarkStringAlphanumericUnicode(b *testing.B) { for _, tc := range stringAlphanumericUnicodeTestCases { rule := StringAlphanumericUnicode() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1822,7 +1822,7 @@ func TestStringFQDN(t *testing.T) { func BenchmarkStringFQDN(b *testing.B) { for _, tc := range stringFQDNTestCases { rule := StringFQDN() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } } @@ -1899,7 +1899,7 @@ func TestStringKubernetesQualifiedName(t *testing.T) { func BenchmarkStringKubernetesQualifiedName(b *testing.B) { for _, tc := range stringK8sQualifiedNameTestCases { rule := StringKubernetesQualifiedName() - for range b.N { + for b.Loop() { _ = rule.Validate(tc.in) } }