diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc.go index b9baa2dc228..a46f406b160 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc.go @@ -56,10 +56,20 @@ type StructSlice struct { // +k8s:listMapKey=key // +k8s:eachVal=+k8s:validateFalse="field MapSliceNonComparableField[*]" MapSliceNonComparableField []NonComparableStructWithKey `json:"mapSliceNonComparableField"` + + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:eachVal=+k8s:validateFalse="field MapSlicePtrKeyField[*]" + MapSlicePtrKeyField []PtrKeyStruct `json:"mapSlicePtrKeyField"` + + // +k8s:listType=map + // +k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + // +k8s:eachVal=+k8s:validateFalse="field MapSliceMixedKeyField[*]" + MapSliceMixedKeyField []MixedKeyStruct `json:"mapSliceMixedKeyField"` } type StringType string - type IntSliceType []int type ComparableStruct struct { @@ -81,3 +91,16 @@ type NonComparableStructWithKey struct { Key string `json:"key"` IntPtrField *int `json:"intPtrField"` } + +// +k8s:validateFalse="type PtrKeyStruct" +type PtrKeyStruct struct { + Key *string `json:"key"` + Data string `json:"data"` +} + +// +k8s:validateFalse="type MixedKeyStruct" +type MixedKeyStruct 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/ratcheting/list/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc_test.go index 8a05f169d2b..08e3b9c9d8c 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/ratcheting/list/doc_test.go @@ -34,8 +34,9 @@ func Test_StructSlice(t *testing.T) { SetSliceNonComparableField: []NonComparableStruct{{IntPtrField: ptr.To(1)}}, MapSliceComparableField: []ComparableStructWithKey{{Key: "x", IntField: 1}}, MapSliceNonComparableField: []NonComparableStructWithKey{{Key: "x", IntPtrField: ptr.To(1)}}, + MapSlicePtrKeyField: []PtrKeyStruct{{Key: ptr.To("x"), Data: "y"}}, + MapSliceMixedKeyField: []MixedKeyStruct{{Key1: ptr.To("x"), Key2: "y", Data: "z"}}, } - st.Value(invalidStructSlice).ExpectValidateFalseByPath(map[string][]string{ "atomicSliceStringField[0]": {"field AtomicSliceStringField[*]"}, "atomicSliceTypeField[0]": {"field AtomicSliceTypeField[*]"}, @@ -45,6 +46,8 @@ func Test_StructSlice(t *testing.T) { "setSliceNonComparableField[0]": {"field SetSliceNonComparableField[*]", "type NonComparableStruct"}, "mapSliceComparableField[0]": {"field MapSliceComparableField[*]"}, "mapSliceNonComparableField[0]": {"field MapSliceNonComparableField[*]", "type NonComparableStructWithKey"}, + "mapSlicePtrKeyField[0]": {"field MapSlicePtrKeyField[*]", "type PtrKeyStruct"}, + "mapSliceMixedKeyField[0]": {"field MapSliceMixedKeyField[*]", "type MixedKeyStruct"}, }) // No changes. @@ -60,6 +63,8 @@ func Test_StructSlice(t *testing.T) { SetSliceNonComparableField: []NonComparableStruct{{IntPtrField: ptr.To(1)}}, MapSliceComparableField: []ComparableStructWithKey{{Key: "x", IntField: 1}}, MapSliceNonComparableField: []NonComparableStructWithKey{{Key: "x", IntPtrField: ptr.To(1)}}, + MapSlicePtrKeyField: []PtrKeyStruct{{Key: ptr.To("x"), Data: "y"}}, + MapSliceMixedKeyField: []MixedKeyStruct{{Key1: ptr.To("x"), Key2: "y", Data: "z"}}, }).OldValue(&StructSlice{ AtomicSliceStringField: []StringType{"", "x"}, AtomicSliceTypeField: IntSliceType{1, 2}, @@ -69,8 +74,9 @@ func Test_StructSlice(t *testing.T) { SetSliceNonComparableField: []NonComparableStruct{{IntPtrField: ptr.To(2)}, {IntPtrField: ptr.To(1)}}, MapSliceComparableField: []ComparableStructWithKey{{Key: "y", IntField: 2}, {Key: "x", IntField: 1}}, MapSliceNonComparableField: []NonComparableStructWithKey{{Key: "y", IntPtrField: ptr.To(2)}, {Key: "x", IntPtrField: ptr.To(1)}}, - }).ExpectValidateFalseByPath(map[string][]string{ - "atomicSliceStringField[0]": {"field AtomicSliceStringField[*]"}, + MapSlicePtrKeyField: []PtrKeyStruct{{Key: ptr.To("a"), Data: "b"}, {Key: ptr.To("x"), Data: "y"}}, + MapSliceMixedKeyField: []MixedKeyStruct{{Key1: ptr.To("a"), Key2: "b", Data: "c"}, {Key1: ptr.To("x"), Key2: "y", Data: "z"}}, + }).ExpectValidateFalseByPath(map[string][]string{"atomicSliceStringField[0]": {"field AtomicSliceStringField[*]"}, "atomicSliceTypeField[0]": {"field AtomicSliceTypeField[*]"}, "atomicSliceComparableField[0]": {"field AtomicSliceComparableField[*]"}, "atomicSliceNonComparableField[0]": {"field AtomicSliceNonComparableField[*]", "type NonComparableStruct"}, @@ -82,10 +88,14 @@ func Test_StructSlice(t *testing.T) { SetSliceNonComparableField: []NonComparableStruct{{IntPtrField: ptr.To(2)}, {IntPtrField: ptr.To(1)}}, MapSliceComparableField: []ComparableStructWithKey{{Key: "y", IntField: 2}, {Key: "x", IntField: 1}}, MapSliceNonComparableField: []NonComparableStructWithKey{{Key: "y", IntPtrField: ptr.To(2)}, {Key: "x", IntPtrField: ptr.To(1)}}, + MapSlicePtrKeyField: []PtrKeyStruct{{Key: ptr.To("b"), Data: "2"}, {Key: ptr.To("a"), Data: "1"}}, + MapSliceMixedKeyField: []MixedKeyStruct{{Key1: ptr.To("b"), Key2: "2", Data: "B"}, {Key1: ptr.To("a"), Key2: "1", Data: "A"}}, }).OldValue(&StructSlice{ SetSliceComparableField: []ComparableStruct{{IntField: 1}, {IntField: 2}}, SetSliceNonComparableField: []NonComparableStruct{{IntPtrField: ptr.To(1)}, {IntPtrField: ptr.To(2)}}, MapSliceComparableField: []ComparableStructWithKey{{Key: "x", IntField: 1}, {Key: "y", IntField: 2}}, MapSliceNonComparableField: []NonComparableStructWithKey{{Key: "x", IntPtrField: ptr.To(1)}, {Key: "y", IntPtrField: ptr.To(2)}}, + MapSlicePtrKeyField: []PtrKeyStruct{{Key: ptr.To("a"), Data: "1"}, {Key: ptr.To("b"), Data: "2"}}, + MapSliceMixedKeyField: []MixedKeyStruct{{Key1: ptr.To("a"), Key2: "1", Data: "A"}, {Key1: ptr.To("b"), Key2: "2", Data: "B"}}, }).ExpectValid() } 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 f211c86d459..08c0f731d7f 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 @@ -49,6 +49,17 @@ func RegisterValidations(scheme *testscheme.Scheme) error { return nil } +// Validate_MixedKeyStruct validates an instance of MixedKeyStruct according +// to declarative validation rules in the API schema. +func Validate_MixedKeyStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedKeyStruct) (errs field.ErrorList) { + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type MixedKeyStruct")...) + + // field MixedKeyStruct.Key1 has no validation + // field MixedKeyStruct.Key2 has no validation + // field MixedKeyStruct.Data has no validation + return errs +} + // Validate_NonComparableStruct validates an instance of NonComparableStruct according // to declarative validation rules in the API schema. func Validate_NonComparableStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStruct) (errs field.ErrorList) { @@ -68,6 +79,16 @@ func Validate_NonComparableStructWithKey(ctx context.Context, op operation.Opera return errs } +// Validate_PtrKeyStruct validates an instance of PtrKeyStruct according +// to declarative validation rules in the API schema. +func Validate_PtrKeyStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *PtrKeyStruct) (errs field.ErrorList) { + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "type PtrKeyStruct")...) + + // field PtrKeyStruct.Key has no validation + // field PtrKeyStruct.Data has no validation + return errs +} + // Validate_StructSlice validates an instance of StructSlice according // to declarative validation rules in the API schema. func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *StructSlice) (errs field.ErrorList) { @@ -199,5 +220,53 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath * return }(fldPath.Child("mapSliceNonComparableField"), obj.MapSliceNonComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []NonComparableStructWithKey { return oldObj.MapSliceNonComparableField }))...) + // field StructSlice.MapSlicePtrKeyField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrKeyStruct) (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.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key)) + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *PtrKeyStruct) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSlicePtrKeyField[*]") + })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key)) + })...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key)) + }, validate.SemanticDeepEqual, Validate_PtrKeyStruct)...) + return + }(fldPath.Child("mapSlicePtrKeyField"), obj.MapSlicePtrKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []PtrKeyStruct { return oldObj.MapSlicePtrKeyField }))...) + + // field StructSlice.MapSliceMixedKeyField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MixedKeyStruct) (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.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a MixedKeyStruct, b MixedKeyStruct) bool { + return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && a.Key2 == b.Key2 + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedKeyStruct) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSliceMixedKeyField[*]") + })...) + // lists with map semantics require unique keys + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a MixedKeyStruct, b MixedKeyStruct) bool { + return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && a.Key2 == b.Key2 + })...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a MixedKeyStruct, b MixedKeyStruct) bool { + return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && a.Key2 == b.Key2 + }, validate.SemanticDeepEqual, Validate_MixedKeyStruct)...) + return + }(fldPath.Child("mapSliceMixedKeyField"), obj.MapSliceMixedKeyField, safe.Field(oldObj, func(oldObj *StructSlice) []MixedKeyStruct { return oldObj.MapSliceMixedKeyField }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc.go index b3d9863ff60..5d3c40fefdf 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc.go @@ -41,6 +41,19 @@ type Struct struct { // +k8s:listMapKey=boolKey // +k8s:item(boolKey: true, stringKey: "target", intKey: 42)=+k8s:validateFalse="item OutOfOrder[boolKey=42,stringKey=target,intKey=42]" OutOfOrder []Item `json:"outOfOrder"` + + // +k8s:listType=map + // +k8s:listMapKey=stringKey + // +k8s:listMapKey=intKey + // +k8s:listMapKey=boolKey + // +k8s:item(stringKey: "target-ptr", intKey: 42, boolKey: true)=+k8s:validateFalse="item PtrItems[stringKey=target-ptr,intKey=42,boolKey=true]" + PtrItems []PtrItem `json:"ptrItems"` + + // +k8s:listType=map + // +k8s:listMapKey=stringPtrKey + // +k8s:listMapKey=stringKey + // +k8s:item(stringPtrKey: "target-ptr", stringKey: "target")=+k8s:validateFalse="item MixedPtrItems" + MixedPtrItems []MixedPtrItem `json:"mixedPtrItems"` } type Item struct { @@ -49,3 +62,16 @@ type Item struct { BoolKey bool `json:"boolKey"` Data string `json:"data"` } + +type PtrItem struct { + StringKey *string `json:"stringKey"` + IntKey int `json:"intKey"` + BoolKey bool `json:"boolKey"` + Data string `json:"data"` +} + +type MixedPtrItem struct { + StringPtrKey *string `json:"stringPtrKey"` + StringKey string `json:"stringKey"` + Data string `json:"data"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc_test.go index f9b2cbfb892..4354b839232 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/doc_test.go @@ -18,6 +18,8 @@ package multiplekeys import ( "testing" + + "k8s.io/utils/ptr" ) func Test(t *testing.T) { @@ -71,4 +73,30 @@ func Test(t *testing.T) { {StringKey: "other", IntKey: 1, BoolKey: false}, }, }).ExpectValid() + + st.Value(&Struct{ + PtrItems: []PtrItem{ + {StringKey: ptr.To("target-ptr"), IntKey: 42, BoolKey: true, Data: "match"}, + {StringKey: ptr.To("target-ptr"), IntKey: 42, BoolKey: false, Data: "no match, bool differs"}, + {StringKey: ptr.To("target-ptr"), IntKey: 99, BoolKey: true, Data: "no match, int differs"}, + {StringKey: ptr.To("other"), IntKey: 42, BoolKey: true, Data: "no match, string differs"}, + {StringKey: nil, IntKey: 42, BoolKey: true, Data: "no match, nil string"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `ptrItems[0]`: { + "item PtrItems[stringKey=target-ptr,intKey=42,boolKey=true]", + }, + }) + + st.Value(&Struct{ + MixedPtrItems: []MixedPtrItem{ + {StringPtrKey: ptr.To("target-ptr"), StringKey: "target", Data: "match"}, + {StringPtrKey: ptr.To("target-ptr"), StringKey: "other", Data: "no match"}, + {StringPtrKey: nil, StringKey: "target", Data: "no match"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `mixedPtrItems[0]`: { + "item MixedPtrItems", + }, + }) } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go index 3be43db1556..6265d502ece 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/item/multiple_keys/zz_generated.validations.go @@ -97,5 +97,49 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("outOfOrder"), obj.OutOfOrder, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.OutOfOrder }))...) + // field Struct.PtrItems + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrItem) (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 PtrItem, b PtrItem) bool { + return ((a.StringKey == nil && b.StringKey == nil) || (a.StringKey != nil && b.StringKey != nil && *a.StringKey == *b.StringKey)) && a.IntKey == b.IntKey && a.BoolKey == b.BoolKey + })...) + func() { // cohort {"stringKey": "target-ptr", "intKey": 42, "boolKey": true} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *PtrItem) bool { + return item.StringKey != nil && *item.StringKey == "target-ptr" && item.IntKey == 42 && item.BoolKey == true + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *PtrItem) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item PtrItems[stringKey=target-ptr,intKey=42,boolKey=true]") + })...) + }() + return + }(fldPath.Child("ptrItems"), obj.PtrItems, safe.Field(oldObj, func(oldObj *Struct) []PtrItem { return oldObj.PtrItems }))...) + + // field Struct.MixedPtrItems + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MixedPtrItem) (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 MixedPtrItem, b MixedPtrItem) bool { + return ((a.StringPtrKey == nil && b.StringPtrKey == nil) || (a.StringPtrKey != nil && b.StringPtrKey != nil && *a.StringPtrKey == *b.StringPtrKey)) && a.StringKey == b.StringKey + })...) + func() { // cohort {"stringPtrKey": "target-ptr", "stringKey": "target"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MixedPtrItem) bool { + return item.StringPtrKey != nil && *item.StringPtrKey == "target-ptr" && item.StringKey == "target" + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedPtrItem) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item MixedPtrItems") + })...) + }() + return + }(fldPath.Child("mixedPtrItems"), obj.MixedPtrItems, safe.Field(oldObj, func(oldObj *Struct) []MixedPtrItem { return oldObj.MixedPtrItems }))...) + return errs } 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 1dcd6829e31..d636653da19 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 @@ -57,6 +57,11 @@ type Struct struct { // +k8s:listMapKey=key // +k8s:item(key: "target")=+k8s:validateFalse="item AtomicUniqueMapItems[key=target]" AtomicUniqueMapItems []Item `json:"atomicUniqueMapItems"` + + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:item(key: "target-ptr")=+k8s:validateFalse="item PtrKeyItems[key=target-ptr]" + PtrKeyItems []PtrKeyItem `json:"ptrKeyItems"` } type StructWithNestedTypedef struct { @@ -95,3 +100,8 @@ type NestedTypedefItem struct { Key StringAlias `json:"key"` Name string `json:"name"` } + +type PtrKeyItem struct { + Key *string `json:"key"` + Data string `json:"data"` +} 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 24ff9c55f0d..4377e16960c 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 @@ -18,6 +18,8 @@ package singlekey import ( "testing" + + "k8s.io/utils/ptr" ) func Test(t *testing.T) { @@ -142,4 +144,24 @@ func Test(t *testing.T) { st.Value(&Struct{ AtomicUniqueMapItems: nil, }).ExpectValid() + + st.Value(&Struct{ + PtrKeyItems: []PtrKeyItem{ + {Key: ptr.To("a"), Data: "d1"}, + {Key: ptr.To("target-ptr"), Data: "d2"}, + {Key: nil, Data: "d3"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `ptrKeyItems[1]`: { + "item PtrKeyItems[key=target-ptr]", + }, + }) + + st.Value(&Struct{ + PtrKeyItems: []PtrKeyItem{ + {Key: ptr.To("a"), Data: "d1"}, + {Key: ptr.To("b"), Data: "d2"}, + {Key: nil, Data: "d3"}, + }, + }).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 133d7b567aa..d3da78237a0 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 @@ -164,6 +164,26 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("atomicUniqueMapItems"), obj.AtomicUniqueMapItems, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicUniqueMapItems }))...) + // field Struct.PtrKeyItems + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrKeyItem) (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 PtrKeyItem, b PtrKeyItem) bool { + return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key)) + })...) + func() { // cohort {"key": "target-ptr"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *PtrKeyItem) bool { return item.Key != nil && *item.Key == "target-ptr" }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *PtrKeyItem) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item PtrKeyItems[key=target-ptr]") + })...) + }() + return + }(fldPath.Child("ptrKeyItems"), obj.PtrKeyItems, safe.Field(oldObj, func(oldObj *Struct) []PtrKeyItem { return oldObj.PtrKeyItems }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc.go index b1a4095427c..faaf76dcc06 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc.go @@ -53,6 +53,18 @@ type Struct struct { // +k8s:listMapKey=key2Field // +k8s:eachVal=+k8s:validateFalse="field Struct.ListNonComparableField[*]" ListNonComparableField []NonComparableStruct `json:"listNonComparableField"` + + // +k8s:listType=map + // +k8s:listMapKey=key1Field + // +k8s:listMapKey=key2Field + // +k8s:item(key1Field: "target-ptr", key2Field: 42)=+k8s:validateFalse="item ListPtrKeyField[key1Field=target-ptr,key2Field=42]" + ListPtrKeyField []PtrKeyStruct `json:"listPtrKeyField"` + + // +k8s:listType=map + // +k8s:listMapKey=stringPtrKey + // +k8s:listMapKey=stringKey + // +k8s:item(stringPtrKey: "target-ptr", stringKey: "target")=+k8s:validateFalse="item ListMixedPtrKeyField" + ListMixedPtrKeyField []MixedPtrKeyStruct `json:"listMixedPtrKeyField"` } type OtherStruct struct { @@ -73,3 +85,15 @@ type NonComparableStruct struct { // +k8s:listMapKey=key1Field // +k8s:listMapKey=key2Field type ListType []OtherStruct + +type PtrKeyStruct struct { + Key1Field *string `json:"key1Field"` + Key2Field int `json:"key2Field"` + DataField string `json:"dataField"` +} + +type MixedPtrKeyStruct struct { + StringPtrKey *string `json:"stringPtrKey"` + StringKey string `json:"stringKey"` + DataField string `json:"dataField"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go index c11f88c17c8..bb41132f259 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/doc_test.go @@ -20,6 +20,7 @@ import ( "testing" field "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" ) func TestUniqueness(t *testing.T) { @@ -162,3 +163,39 @@ func TestRatcheting(t *testing.T) { st.Value(&struct1).OldValue(&struct2).ExpectValid() st.Value(&struct2).OldValue(&struct1).ExpectValid() } + +func TestItemWithPtrKey(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + ListPtrKeyField: []PtrKeyStruct{ + {Key1Field: ptr.To("target-ptr"), Key2Field: 42, DataField: "match"}, + {Key1Field: ptr.To("target-ptr"), Key2Field: 99, DataField: "no match, int differs"}, + {Key1Field: ptr.To("other"), Key2Field: 42, DataField: "no match, string differs"}, + {Key1Field: nil, Key2Field: 42, DataField: "no match, nil string"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `listPtrKeyField[0]`: { + "item ListPtrKeyField[key1Field=target-ptr,key2Field=42]", + }, + }) + + st.Value(&Struct{ + ListPtrKeyField: []PtrKeyStruct{ + {Key1Field: ptr.To("other"), Key2Field: 42, DataField: "d1"}, + {Key1Field: nil, Key2Field: 99, DataField: "d2"}, + }, + }).ExpectValid() + + st.Value(&Struct{ + ListMixedPtrKeyField: []MixedPtrKeyStruct{ + {StringPtrKey: ptr.To("target-ptr"), StringKey: "target", DataField: "match"}, + {StringPtrKey: ptr.To("target-ptr"), StringKey: "other", DataField: "no match"}, + {StringPtrKey: nil, StringKey: "target", DataField: "no match"}, + }, + }).ExpectValidateFalseByPath(map[string][]string{ + `listMixedPtrKeyField[0]`: { + "item ListMixedPtrKeyField", + }, + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go index 84a5681234b..77f848de273 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/multiple_keys/zz_generated.validations.go @@ -178,5 +178,49 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...) + // field Struct.ListPtrKeyField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrKeyStruct) (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 PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.Key1Field == nil && b.Key1Field == nil) || (a.Key1Field != nil && b.Key1Field != nil && *a.Key1Field == *b.Key1Field)) && a.Key2Field == b.Key2Field + })...) + func() { // cohort {"key1Field": "target-ptr", "key2Field": 42} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *PtrKeyStruct) bool { + return item.Key1Field != nil && *item.Key1Field == "target-ptr" && item.Key2Field == 42 + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *PtrKeyStruct) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ListPtrKeyField[key1Field=target-ptr,key2Field=42]") + })...) + }() + return + }(fldPath.Child("listPtrKeyField"), obj.ListPtrKeyField, safe.Field(oldObj, func(oldObj *Struct) []PtrKeyStruct { return oldObj.ListPtrKeyField }))...) + + // field Struct.ListMixedPtrKeyField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MixedPtrKeyStruct) (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 MixedPtrKeyStruct, b MixedPtrKeyStruct) bool { + return ((a.StringPtrKey == nil && b.StringPtrKey == nil) || (a.StringPtrKey != nil && b.StringPtrKey != nil && *a.StringPtrKey == *b.StringPtrKey)) && a.StringKey == b.StringKey + })...) + func() { // cohort {"stringPtrKey": "target-ptr", "stringKey": "target"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MixedPtrKeyStruct) bool { + return item.StringPtrKey != nil && *item.StringPtrKey == "target-ptr" && item.StringKey == "target" + }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedPtrKeyStruct) field.ErrorList { + return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item ListMixedPtrKeyField") + })...) + }() + return + }(fldPath.Child("listMixedPtrKeyField"), obj.ListMixedPtrKeyField, safe.Field(oldObj, func(oldObj *Struct) []MixedPtrKeyStruct { return oldObj.ListMixedPtrKeyField }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc.go index d32f335557b..5627f4f7daf 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc.go @@ -49,6 +49,10 @@ type Struct struct { // +k8s:listMapKey=keyField // +k8s:eachVal=+k8s:validateFalse="field Struct.ListNonComparableField[*]" ListNonComparableField []NonComparableStruct `json:"listNonComparableField"` + + // +k8s:listType=map + // +k8s:listMapKey=keyField + ListPtrKeyField []PtrKeyStruct `json:"listPtrKeyField"` } type OtherStruct struct { @@ -63,6 +67,11 @@ type NonComparableStruct struct { StringPtrField *string `json:"stringPtrField"` } +type PtrKeyStruct struct { + KeyField *string `json:"keyField"` + DataField string `json:"dataField"` +} + // +k8s:listType=map // +k8s:listMapKey=keyField type ListType []OtherStruct diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go index 49f724db8b9..01a2a782a9b 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/doc_test.go @@ -162,3 +162,20 @@ func TestRatcheting(t *testing.T) { st.Value(&struct1).OldValue(&struct2).ExpectValid() st.Value(&struct2).OldValue(&struct1).ExpectValid() } + +func TestUniquenessPtrKey(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + ListPtrKeyField: []PtrKeyStruct{ + {ptr.To("key1"), "one"}, + {ptr.To("key2"), "two"}, + {ptr.To("key2"), "three"}, // duplicate key + {nil, "four"}, + {nil, "five"}, // duplicate nil key + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ + field.Duplicate(field.NewPath("listPtrKeyField").Index(2), nil), + field.Duplicate(field.NewPath("listPtrKeyField").Index(4), nil), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go index fd68faf531d..4aa7de5123e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/listmap/single_key/zz_generated.validations.go @@ -158,5 +158,20 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...) + // field Struct.ListPtrKeyField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrKeyStruct) (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 PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.KeyField == nil && b.KeyField == nil) || (a.KeyField != nil && b.KeyField != nil && *a.KeyField == *b.KeyField)) + })...) + return + }(fldPath.Child("listPtrKeyField"), obj.ListPtrKeyField, safe.Field(oldObj, func(oldObj *Struct) []PtrKeyStruct { return oldObj.ListPtrKeyField }))...) + return errs } 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 index c6b95fcd9b9..2fa2b69df23 100644 --- 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 @@ -60,6 +60,26 @@ type Struct struct { // +k8s:listMapKey=key // +k8s:customUnique CustomUniqueListWithTypeMap []Item `json:"customUniqueListWithTypeMap"` + + // unique=map with pointer key + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key + SliceMapFieldWithPtrKey []PtrKeyStruct `json:"sliceMapFieldWithPtrKey"` + + // unique=map with mixed (pointer and primitive) keys + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + SliceMapFieldWithMixedKeys []ItemWithMixedKeys `json:"sliceMapFieldWithMixedKeys"` + + // unique=map with multiple pointer keys + // +k8s:listType=atomic + // +k8s:unique=map + // +k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + SliceMapFieldWithMultiplePtrKeys []ItemWithMultiplePtrKeys `json:"sliceMapFieldWithMultiplePtrKeys"` } type Item struct { @@ -72,3 +92,20 @@ type ItemWithMultipleKeys struct { Key2 string `json:"key2"` Data string `json:"data"` } + +type PtrKeyStruct struct { + Key *string `json:"key"` + Data string `json:"data"` +} + +type ItemWithMixedKeys struct { + Key1 *string `json:"key1"` + Key2 string `json:"key2"` + Data string `json:"data"` +} + +type ItemWithMultiplePtrKeys 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 index 3d8c32e9921..a523d247ba4 100644 --- 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 @@ -20,6 +20,7 @@ import ( "testing" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" ) func TestUnique(t *testing.T) { @@ -45,26 +46,42 @@ func TestUnique(t *testing.T) { }, CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, + SliceMapFieldWithPtrKey: []PtrKeyStruct{ + {Key: ptr.To("a"), Data: "first"}, + {Key: ptr.To("b"), Data: "second"}, + }, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{ + {Key1: ptr.To("a"), Key2: "x", Data: "first"}, + {Key1: ptr.To("a"), Key2: "y", Data: "second"}, + }, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{ + {Key1: ptr.To("a"), Key2: ptr.To("x"), Data: "first"}, + {Key1: ptr.To("a"), Key2: ptr.To("y"), Data: "second"}, + }, }).ExpectValid() // Test empty lists st.Value(&Struct{ - PrimitiveListUniqueSet: []string{}, - SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{}, - AtomicListUniqueSet: []Item{}, - AtomicListUniqueMap: []Item{}, - CustomUniqueListWithTypeSet: []string{}, - CustomUniqueListWithTypeMap: []Item{}, + PrimitiveListUniqueSet: []string{}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{}, + AtomicListUniqueSet: []Item{}, + AtomicListUniqueMap: []Item{}, + CustomUniqueListWithTypeSet: []string{}, + CustomUniqueListWithTypeMap: []Item{}, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{}, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{}, }).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"}}, - CustomUniqueListWithTypeSet: []string{"single"}, - CustomUniqueListWithTypeMap: []Item{{Key: "single"}}, + PrimitiveListUniqueSet: []string{"single"}, + SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{{Key1: "a", Key2: "b", Data: "one"}}, + AtomicListUniqueSet: []Item{{Key: "single", Data: "one"}}, + AtomicListUniqueMap: []Item{{Key: "single", Data: "one"}}, + CustomUniqueListWithTypeSet: []string{"single"}, + CustomUniqueListWithTypeMap: []Item{{Key: "single"}}, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{{Key1: ptr.To("a"), Key2: "b", Data: "one"}}, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{{Key1: ptr.To("a"), Key2: ptr.To("b"), Data: "one"}}, }).ExpectValid() // Test duplicate values (should fail validation) @@ -87,6 +104,21 @@ func TestUnique(t *testing.T) { }, CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, + SliceMapFieldWithPtrKey: []PtrKeyStruct{ + {Key: ptr.To("a"), Data: "first"}, + {Key: ptr.To("b"), Data: "second"}, + {Key: ptr.To("a"), Data: "third"}, + }, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{ + {Key1: ptr.To("a"), Key2: "x", Data: "first"}, + {Key1: ptr.To("a"), Key2: "y", Data: "second"}, + {Key1: ptr.To("a"), Key2: "x", Data: "third"}, + }, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{ + {Key1: ptr.To("a"), Key2: ptr.To("x"), Data: "first"}, + {Key1: ptr.To("a"), Key2: ptr.To("y"), Data: "second"}, + {Key1: ptr.To("a"), Key2: ptr.To("x"), Data: "third"}, + }, }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{ field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(3), nil), field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(4), nil), @@ -94,6 +126,9 @@ func TestUnique(t *testing.T) { field.Duplicate(field.NewPath("sliceMapFieldWithMultipleKeys").Index(2), nil), field.Duplicate(field.NewPath("atomicListUniqueSet").Index(2), nil), field.Duplicate(field.NewPath("atomicListUniqueMap").Index(2), nil), + field.Duplicate(field.NewPath("sliceMapFieldWithPtrKey").Index(2), nil), + field.Duplicate(field.NewPath("sliceMapFieldWithMixedKeys").Index(2), nil), + field.Duplicate(field.NewPath("sliceMapFieldWithMultiplePtrKeys").Index(2), nil), }) // Test with zero values and empty strings @@ -131,6 +166,18 @@ func TestRatcheting(t *testing.T) { }, CustomUniqueListWithTypeSet: []string{"a", "b", "a"}, CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}}, + SliceMapFieldWithPtrKey: []PtrKeyStruct{ + {Key: ptr.To("a"), Data: "first"}, + {Key: ptr.To("b"), Data: "second"}, + }, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{ + {Key1: ptr.To("a"), Key2: "x", Data: "first"}, + {Key1: ptr.To("a"), Key2: "y", Data: "second"}, + }, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{ + {Key1: ptr.To("a"), Key2: ptr.To("x"), Data: "first"}, + {Key1: ptr.To("a"), Key2: ptr.To("y"), Data: "second"}, + }, } // Same data, different order. @@ -150,6 +197,18 @@ func TestRatcheting(t *testing.T) { }, CustomUniqueListWithTypeSet: []string{"a", "a", "b"}, CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "a"}, {Key: "b"}}, + SliceMapFieldWithPtrKey: []PtrKeyStruct{ + {Key: ptr.To("b"), Data: "second"}, + {Key: ptr.To("a"), Data: "first"}, + }, + SliceMapFieldWithMixedKeys: []ItemWithMixedKeys{ + {Key1: ptr.To("a"), Key2: "y", Data: "second"}, + {Key1: ptr.To("a"), Key2: "x", Data: "first"}, + }, + SliceMapFieldWithMultiplePtrKeys: []ItemWithMultiplePtrKeys{ + {Key1: ptr.To("a"), Key2: ptr.To("y"), Data: "second"}, + {Key1: ptr.To("a"), Key2: ptr.To("x"), Data: "first"}, + }, } // Test that reordering doesn't trigger validation errors 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 index 7e123da08ea..689fd6bc78b 100644 --- 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 @@ -120,5 +120,50 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field return }(fldPath.Child("customUniqueListWithTypeMap"), obj.CustomUniqueListWithTypeMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.CustomUniqueListWithTypeMap }))...) + // field Struct.SliceMapFieldWithPtrKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []PtrKeyStruct) (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 PtrKeyStruct, b PtrKeyStruct) bool { + return ((a.Key == nil && b.Key == nil) || (a.Key != nil && b.Key != nil && *a.Key == *b.Key)) + })...) + return + }(fldPath.Child("sliceMapFieldWithPtrKey"), obj.SliceMapFieldWithPtrKey, safe.Field(oldObj, func(oldObj *Struct) []PtrKeyStruct { return oldObj.SliceMapFieldWithPtrKey }))...) + + // field Struct.SliceMapFieldWithMixedKeys + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ItemWithMixedKeys) (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 ItemWithMixedKeys, b ItemWithMixedKeys) bool { + return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && a.Key2 == b.Key2 + })...) + return + }(fldPath.Child("sliceMapFieldWithMixedKeys"), obj.SliceMapFieldWithMixedKeys, safe.Field(oldObj, func(oldObj *Struct) []ItemWithMixedKeys { return oldObj.SliceMapFieldWithMixedKeys }))...) + + // field Struct.SliceMapFieldWithMultiplePtrKeys + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ItemWithMultiplePtrKeys) (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 ItemWithMultiplePtrKeys, b ItemWithMultiplePtrKeys) bool { + return ((a.Key1 == nil && b.Key1 == nil) || (a.Key1 != nil && b.Key1 != nil && *a.Key1 == *b.Key1)) && ((a.Key2 == nil && b.Key2 == nil) || (a.Key2 != nil && b.Key2 != nil && *a.Key2 == *b.Key2)) + })...) + return + }(fldPath.Child("sliceMapFieldWithMultiplePtrKeys"), obj.SliceMapFieldWithMultiplePtrKeys, safe.Field(oldObj, func(oldObj *Struct) []ItemWithMultiplePtrKeys { return oldObj.SliceMapFieldWithMultiplePtrKeys }))...) + return errs } diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go index 78f1adfbdca..8fc98e33f90 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go @@ -100,7 +100,7 @@ func (itv *itemTagValidator) GetValidations(context Context, tag codetags.Tag) ( // Fields inherit list metadata from typedefs, but not vice-versa. // If we find no listMetadata then something is wrong. // The item tag requires map semantics, which can come from either listType=map or unique=map - if !ok || listMeta.semantic != semanticMap || len(listMeta.keyFields) == 0 { + if !ok || listMeta.semantic != semanticMap || len(listMeta.keyMembers) == 0 { return Validations{}, fmt.Errorf("found items with no list metadata - item tags require listType=map or unique=map with listMapKey") } @@ -270,7 +270,7 @@ func (itv itemTagValidator) Docs() TagDoc { // validateTypeMatch ensures the provided value matches the field's type func validateTypeMatch(fieldType *types.Type, value any) error { - nativeType := util.NativeType(fieldType) + nativeType := util.NonPointer(util.NativeType(fieldType)) switch { case nativeType == types.String: @@ -304,7 +304,11 @@ func buildMatchConditions(elemT *types.Type, criteria []keyValuePair, itemRef st if err != nil { return "", err } - conditions = append(conditions, fmt.Sprintf("%s.%s == %s", itemRef, member.Name, rhs)) + if member.Type.Kind == types.Pointer { + conditions = append(conditions, fmt.Sprintf("%s.%s != nil && *%s.%s == %s", itemRef, member.Name, itemRef, member.Name, rhs)) + } else { + conditions = append(conditions, fmt.Sprintf("%s.%s == %s", itemRef, member.Name, rhs)) + } } return strings.Join(conditions, " && "), nil @@ -324,7 +328,7 @@ func createMatchFn(elemT *types.Type, criteria []keyValuePair) (FunctionLiteral, } func generateComparisonRHS(member *types.Member, value any) (string, error) { - memberType := util.NativeType(member.Type) + memberType := util.NonPointer(util.NativeType(member.Type)) switch { case memberType == types.String: strVal, ok := value.(string) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go index 11e8b2f6b56..1cf86a60999 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go @@ -68,10 +68,10 @@ const ( // listMetadata collects information about a single list with map or set semantics. type listMetadata struct { - ownership listOwnership // For now we don't use it for generation. - semantic listSemantic - keyFields []string // For semantic == map. - keyNames []string // For semantic == map. + ownership listOwnership // For now we don't use it for generation. + semantic listSemantic + keyMembers []*types.Member // For semantic == map. + keyNames []string // For semantic == map. // customUnique indicates that k8s:customUnique is set on this list. // It disables generation of uniqueness validation for this list. @@ -92,13 +92,23 @@ func (lm *listMetadata) makeListMapMatchFunc(t *types.Type) FunctionLiteral { } buf := strings.Builder{} buf.WriteString("return ") - // Note: this does not handle pointer fields, which are not - // supposed to be used as listMap keys. - for i, fld := range lm.keyFields { + + for i, memb := range lm.keyMembers { if i > 0 { buf.WriteString(" && ") } - buf.WriteString(fmt.Sprintf("a.%s == b.%s", fld, fld)) + fldName := memb.Name + + if memb.Type.Kind == types.Pointer { + // Dereference pointers for comparison. + // This is tricky because they could be nil. + // Two keys are equal if all their fields are equal. + // For pointer fields, that means either both are nil, + // or neither is nil and the pointed-to values are equal. + buf.WriteString(fmt.Sprintf("((a.%s == nil && b.%s == nil) || (a.%s != nil && b.%s != nil && *a.%s == *b.%s))", fldName, fldName, fldName, fldName, fldName, fldName)) + } else { + buf.WriteString(fmt.Sprintf("a.%s == b.%s", fldName, fldName)) + } } matchFn.Body = buf.String() return matchFn @@ -210,13 +220,18 @@ func (lmktv listMapKeyTagValidator) GetValidations(context Context, tag codetags return Validations{}, fmt.Errorf("only lists of structs can be list-maps") } - var fieldName string - if memb := util.GetMemberByJSON(util.NativeType(t.Elem), tag.Value); memb == nil { + var memb *types.Member + if m := util.GetMemberByJSON(util.NativeType(t.Elem), tag.Value); m == nil { return Validations{}, fmt.Errorf("no field for JSON name %q", tag.Value) - } else if k := util.NativeType(memb.Type).Kind; k != types.Builtin { - return Validations{}, fmt.Errorf("only primitive types can be list-map keys (%s)", k) } else { - fieldName = memb.Name + keyType := m.Type + if keyType.Kind == types.Pointer { + keyType = keyType.Elem + } + if util.NativeType(keyType).Kind != types.Builtin { + return Validations{}, fmt.Errorf("only primitive types and pointers to primitive types can be list-map keys, not %s", m.Type.String()) + } + memb = m } lm := lmktv.byPath[context.Path.String()] @@ -224,7 +239,7 @@ func (lmktv listMapKeyTagValidator) GetValidations(context Context, tag codetags lm = &listMetadata{} lmktv.byPath[context.Path.String()] = lm } - lm.keyFields = append(lm.keyFields, fieldName) + lm.keyMembers = append(lm.keyMembers, memb) lm.keyNames = append(lm.keyNames, tag.Value) // This tag doesn't generate any validations. It just accumulates @@ -461,12 +476,12 @@ func (lv listValidator) check(lm *listMetadata) error { // Check some fundamental constraints on list tags. // If we have listMapKey but no map semantics, that's an error - if len(lm.keyFields) > 0 && lm.semantic != semanticMap { + if len(lm.keyMembers) > 0 && lm.semantic != semanticMap { return fmt.Errorf("found listMapKey without listType=map or unique=map") } // If we have map semantics but no keys, that's an error - if lm.semantic == semanticMap && len(lm.keyFields) == 0 { + if lm.semantic == semanticMap && len(lm.keyMembers) == 0 { return fmt.Errorf("found listType=map or unique=map without listMapKey") }