From bbdd27d91e5675d25ff7f4c8e11091ae767848c4 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Tue, 9 Sep 2025 22:41:24 +0000 Subject: [PATCH] Add tests for unique tag combo & update-codegen --- .../list/zz_generated.validations.go | 4 +- .../output_tests/tags/item/single_key/doc.go | 7 + .../tags/item/single_key/doc_test.go | 27 ++++ .../single_key/zz_generated.validations.go | 16 ++ .../output_tests/tags/item/typedef/doc.go | 8 + .../tags/item/typedef/doc_test.go | 13 ++ .../item/typedef/zz_generated.validations.go | 26 +++ .../tags/listset/zz_generated.validations.go | 18 +-- .../output_tests/tags/unique/doc.go | 63 ++++++++ .../output_tests/tags/unique/doc_test.go | 148 ++++++++++++++++++ .../tags/unique/zz_generated.validations.go | 110 +++++++++++++ 11 files changed, 429 insertions(+), 11 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go index 0f6ff83269f..280a1ecf46e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/zz_generated.validations.go @@ -142,7 +142,7 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath * errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ComparableStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field SetSliceComparableField[*]") })...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("setSliceComparableField"), obj.SetSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStruct { return oldObj.SetSliceComparableField }))...) @@ -158,7 +158,7 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath * errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStruct) field.ErrorList { return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field SetSliceNonComparableField[*]") })...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual)...) // iterate the list and call the type's validation function errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual, nil, Validate_NonComparableStruct)...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc.go index 7950660c4e9..9fc1151bad7 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc.go @@ -46,6 +46,13 @@ type Struct struct { // +k8s:listMapKey=id // +k8s:item(id: "typedef-target")=+k8s:validateFalse="item TypedefItems[id=typedef-target]" TypedefItems TypedefItemList `json:"typedefItems"` + + // Test atomic + unique=map + item combination + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key + // +k8s:item(key: "target")=+k8s:validateFalse="item AtomicUniqueMapItems[key=target]" + AtomicUniqueMapItems []Item `json:"atomicUniqueMapItems"` } type StructWithNestedTypedef struct { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc_test.go index d568f48b285..c9793d76e93 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/doc_test.go @@ -103,4 +103,31 @@ func Test(t *testing.T) { {Key: "b", Name: "n2"}, }, }).ExpectValid() + + // Test atomic + unique=map + item combination + st.Value(&Struct{ + AtomicUniqueMapItems: []Item{ + {Key: "a", Data: "d1"}, + {Key: "target", Data: "d2"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `atomicUniqueMapItems[1]`: { + "item AtomicUniqueMapItems[key=target]", + }, + }) + + st.Value(&Struct{ + AtomicUniqueMapItems: []Item{ + {Key: "a", Data: "d1"}, + {Key: "b", Data: "d2"}, + }, + }).ExpectValid() + + st.Value(&Struct{ + AtomicUniqueMapItems: []Item{}, + }).ExpectValid() + + st.Value(&Struct{ + AtomicUniqueMapItems: nil, + }).ExpectValid() } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go index c3a872386ee..09fc16b52da 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/single_key/zz_generated.validations.go @@ -118,6 +118,22 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("typedefItems"), obj.TypedefItems, safe.Field(oldObj, func(oldObj *Struct) TypedefItemList { return oldObj.TypedefItems }))...) + // field Struct.AtomicUniqueMapItems + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item AtomicUniqueMapItems[key=target]") + })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) + return + }(fldPath.Child("atomicUniqueMapItems"), obj.AtomicUniqueMapItems, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicUniqueMapItems }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc.go index 5058fb2c4c5..bc49214121b 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc.go @@ -32,6 +32,9 @@ type Struct struct { // +k8s:item(id: "field-target")=+k8s:validateFalse="item DualItems[id=field-target] from field" DualItems DualItemList `json:"dualItems"` + + // +k8s:item(id: "target")=+k8s:validateFalse="item ConflictingItems[id=target] from field" + ConflictingItems ConflictingItemList `json:"conflictingItems"` } type Item struct { @@ -59,3 +62,8 @@ type DualItem struct { // +k8s:listMapKey=id // +k8s:item(id: "typedef-target")=+k8s:validateFalse="item DualItems[id=typedef-target] from typedef" type DualItemList []DualItem + +// +k8s:listType=map +// +k8s:listMapKey=id +// +k8s:item(id: "target")=+k8s:validateFalse="item ConflictingItems[id=target] from typedef" +type ConflictingItemList []DualItem diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc_test.go index 61f73b72bb9..0fb31bc28e6 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/doc_test.go @@ -77,4 +77,17 @@ func Test(t *testing.T) { `dualItems[1]`: {"item DualItems[id=typedef-target] from typedef"}, `dualItems[2]`: {"item DualItems[id=field-target] from field"}, }) + + // Test tag on field and typedef with same key. + st.Value(&Struct{ + ConflictingItems: ConflictingItemList{ + {ID: "a", Name: "n1"}, + {ID: "target", Name: "n2"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `conflictingItems[1]`: { + "item ConflictingItems[id=target] from typedef", + "item ConflictingItems[id=target] from field", + }, + }) } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go index dec51b30f70..670a53cc76a 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/typedef/zz_generated.validations.go @@ -49,6 +49,16 @@ func RegisterValidations(scheme *testscheme.Scheme) error { return nil } +// Validate_ConflictingItemList validates an instance of ConflictingItemList according +// to declarative validation rules in the API schema. +func Validate_ConflictingItemList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj ConflictingItemList) (errs field.ErrorList) { + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *DualItem) bool { return item.ID == "target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *DualItem) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ConflictingItems[id=target] from typedef") + })...) + + return errs +} + // Validate_DualItemList validates an instance of DualItemList according // to declarative validation rules in the API schema. func Validate_DualItemList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj DualItemList) (errs field.ErrorList) { @@ -125,5 +135,21 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("dualItems"), obj.DualItems, safe.Field(oldObj, func(oldObj *Struct) DualItemList { return oldObj.DualItems }))...) + // field Struct.ConflictingItems + errs = append(errs, + func(fldPath *field.Path, obj, oldObj ConflictingItemList) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *DualItem) bool { return item.ID == "target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *DualItem) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ConflictingItems[id=target] from field") + })...) + // call the type's validation function + errs = append(errs, Validate_ConflictingItemList(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("conflictingItems"), obj.ConflictingItems, safe.Field(oldObj, func(oldObj *Struct) ConflictingItemList { return oldObj.ConflictingItems }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listset/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listset/zz_generated.validations.go index c7a5a149e94..55a47c07f2d 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listset/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listset/zz_generated.validations.go @@ -83,7 +83,7 @@ func Validate_ImmutableStruct(ctx context.Context, op operation.Operation, fldPa } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, validate.ImmutableByCompare)...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("sliceSetComparableField"), obj.SliceSetComparableField, safe.Field(oldObj, func(oldObj *ImmutableStruct) []ComparableStruct { return oldObj.SliceSetComparableField }))...) @@ -109,7 +109,7 @@ func Validate_ImmutableStruct(ctx context.Context, op operation.Operation, fldPa } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual, nil, validate.ImmutableByReflect)...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual)...) return }(fldPath.Child("sliceSetNonComparableField"), obj.SliceSetNonComparableField, safe.Field(oldObj, func(oldObj *ImmutableStruct) []NonComparableStruct { return oldObj.SliceSetNonComparableField }))...) @@ -135,7 +135,7 @@ func Validate_ImmutableStruct(ctx context.Context, op operation.Operation, fldPa } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, validate.ImmutableByCompare)...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("sliceSetPrimitiveField"), obj.SliceSetPrimitiveField, safe.Field(oldObj, func(oldObj *ImmutableStruct) []int { return oldObj.SliceSetPrimitiveField }))...) @@ -149,7 +149,7 @@ func Validate_ImmutableStruct(ctx context.Context, op operation.Operation, fldPa } // call field-attached validations errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual, nil, validate.ImmutableByReflect)...) - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual)...) return }(fldPath.Child("sliceSetFalselyComparableField"), obj.SliceSetFalselyComparableField, safe.Field(oldObj, func(oldObj *ImmutableStruct) []FalselyComparableStruct { return oldObj.SliceSetFalselyComparableField }))...) @@ -170,7 +170,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("sliceStringField"), obj.SliceStringField, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.SliceStringField }))...) @@ -183,7 +183,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("sliceIntField"), obj.SliceIntField, safe.Field(oldObj, func(oldObj *Struct) []int { return oldObj.SliceIntField }))...) @@ -196,7 +196,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) return }(fldPath.Child("sliceComparableField"), obj.SliceComparableField, safe.Field(oldObj, func(oldObj *Struct) []ComparableStruct { return oldObj.SliceComparableField }))...) @@ -209,7 +209,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual)...) return }(fldPath.Child("sliceNonComparableField"), obj.SliceNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.SliceNonComparableField }))...) @@ -222,7 +222,7 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return nil } // call field-attached validations - // listType=set requires unique values + // lists with set semantics require unique values errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.SemanticDeepEqual)...) return }(fldPath.Child("sliceFalselyComparableField"), obj.SliceFalselyComparableField, safe.Field(oldObj, func(oldObj *Struct) []FalselyComparableStruct { return oldObj.SliceFalselyComparableField }))...) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go new file mode 100644 index 00000000000..ee5ba38a0e9 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc.go @@ -0,0 +1,63 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// This is a test package. +package unique + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // Basic unique=set on primitive slice + // +k8s:listType=atomic + // +k8s:unique=set + PrimitiveListUniqueSet []string `json:"primitiveListUniqueSet"` + + // unique=map with multiple keys + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + SliceMapFieldWithMultipleKeys []ItemWithMultipleKeys `json:"sliceMapFieldWithMultipleKeys"` + + // atomic + unique=set combination + // +k8s:listType=atomic + // +k8s:unique=set + AtomicListUniqueSet []Item `json:"atomicListUniqueSet"` + + // atomic + unique=map combination + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key + AtomicListUniqueMap []Item `json:"atomicListUniqueMap"` +} + +type Item struct { + Key string `json:"key"` + Data string `json:"data"` +} + +type ItemWithMultipleKeys struct { + Key1 string `json:"key1"` + Key2 string `json:"key2"` + Data string `json:"data"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go new file mode 100644 index 00000000000..adfd6f33425 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/doc_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package unique + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestUnique(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Test empty struct (should be valid) + st.Value(&Struct{}).ExpectValid() + + // Test valid cases with no duplicates + st.Value(&Struct{ + PrimitiveListUniqueSet: []string{"aaa", "bbb"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{ + {Key1: "a", Key2: "x", Data: "first"}, + {Key1: "a", Key2: "y", Data: "second"}, + }, + AtomicListUniqueSet: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + }, + AtomicListUniqueMap: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + }, + }).ExpectValid() + + // Test empty lists + st.Value(&Struct{ + PrimitiveListUniqueSet: []string{}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{}, + AtomicListUniqueSet: []Item{}, + AtomicListUniqueMap: []Item{}, + }).ExpectValid() + + // Test single element lists + st.Value(&Struct{ + PrimitiveListUniqueSet: []string{"single"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{{Key1: "a", Key2: "b", Data: "one"}}, + AtomicListUniqueSet: []Item{{Key: "single", Data: "one"}}, + AtomicListUniqueMap: []Item{{Key: "single", Data: "one"}}, + }).ExpectValid() + + // Test duplicate values (should fail validation) + st.Value(&Struct{ + PrimitiveListUniqueSet: []string{"aaa", "bbb", "ccc", "ccc", "bbb", "aaa"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{ + {Key1: "a", Key2: "x", Data: "first"}, + {Key1: "a", Key2: "y", Data: "second"}, + {Key1: "a", Key2: "x", Data: "third"}, + }, + AtomicListUniqueSet: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + {Key: "key1", Data: "one"}, + }, + AtomicListUniqueMap: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + {Key: "key1", Data: "three"}, + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(3), nil), + field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(4), nil), + field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(5), nil), + field.Duplicate(field.NewPath("sliceMapFieldWithMultipleKeys").Index(2), nil), + field.Duplicate(field.NewPath("atomicListUniqueSet").Index(2), nil), + field.Duplicate(field.NewPath("atomicListUniqueMap").Index(2), nil), + }) + + // Test with zero values and empty strings + st.Value(&Struct{ + PrimitiveListUniqueSet: []string{"", "a", ""}, + AtomicListUniqueMap: []Item{ + {Key: "", Data: "one"}, + {Key: "a", Data: "two"}, + {Key: "", Data: "three"}, + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(2), nil), + field.Duplicate(field.NewPath("atomicListUniqueMap").Index(2), nil), + }) +} + +func TestRatcheting(t *testing.T) { + st := localSchemeBuilder.Test(t) + + struct1 := Struct{ + PrimitiveListUniqueSet: []string{"aaa", "bbb"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{ + {Key1: "a", Key2: "x", Data: "first"}, + {Key1: "a", Key2: "y", Data: "second"}, + }, + AtomicListUniqueSet: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + }, + AtomicListUniqueMap: []Item{ + {Key: "key1", Data: "one"}, + {Key: "key2", Data: "two"}, + }, + } + + // Same data, different order. + struct2 := Struct{ + PrimitiveListUniqueSet: []string{"bbb", "aaa"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{ + {Key1: "a", Key2: "y", Data: "second"}, + {Key1: "a", Key2: "x", Data: "first"}, + }, + AtomicListUniqueSet: []Item{ + {Key: "key2", Data: "two"}, + {Key: "key1", Data: "one"}, + }, + AtomicListUniqueMap: []Item{ + {Key: "key2", Data: "two"}, + {Key: "key1", Data: "one"}, + }, + } + + // Test that reordering doesn't trigger validation errors + st.Value(&struct1).OldValue(&struct2).ExpectValid() + st.Value(&struct2).OldValue(&struct1).ExpectValid() + + // Test that the same data is considered valid regardless of order + st.Value(&struct1).ExpectValid() + st.Value(&struct2).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go new file mode 100644 index 00000000000..0adaccffe45 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/unique/zz_generated.validations.go @@ -0,0 +1,110 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by validation-gen. DO NOT EDIT. + +package unique + +import ( + context "context" + fmt "fmt" + + equality "k8s.io/apimachinery/pkg/api/equality" + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + field "k8s.io/apimachinery/pkg/util/validation/field" + testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme" +) + +func init() { localSchemeBuilder.Register(RegisterValidations) } + +// RegisterValidations adds validation functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterValidations(scheme *testscheme.Scheme) error { + // type Struct + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.PrimitiveListUniqueSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) + return + }(fldPath.Child("primitiveListUniqueSet"), obj.PrimitiveListUniqueSet, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.PrimitiveListUniqueSet }))...) + + // field Struct.SliceMapFieldWithMultipleKeys + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ItemWithMultipleKeys) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a ItemWithMultipleKeys, b ItemWithMultipleKeys) bool { return a.Key1 == b.Key1 && a.Key2 == b.Key2 })...) + return + }(fldPath.Child("sliceMapFieldWithMultipleKeys"), obj.SliceMapFieldWithMultipleKeys, safe.Field(oldObj, func(oldObj *Struct) []ItemWithMultipleKeys { return oldObj.SliceMapFieldWithMultipleKeys }))...) + + // field Struct.AtomicListUniqueSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) + return + }(fldPath.Child("atomicListUniqueSet"), obj.AtomicListUniqueSet, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicListUniqueSet }))...) + + // field Struct.AtomicListUniqueMap + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) { + // don't revalidate unchanged data + if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Item, b Item) bool { return a.Key == b.Key })...) + return + }(fldPath.Child("atomicListUniqueMap"), obj.AtomicListUniqueMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicListUniqueMap }))...) + + return errs +}