Merge pull request #134279 from yongruilin/master_customunique

feat(validation-gen): Introduce k8s:customUnique to control listmap uniqueness
This commit is contained in:
Kubernetes Prow Robot 2025-09-26 10:10:16 -07:00 committed by GitHub
commit 0bdf1f89c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 284 additions and 97 deletions

View file

@ -113,6 +113,7 @@ func Validate_CertificateSigningRequestStatus(ctx context.Context, op operation.
// field certificatesv1.CertificateSigningRequestStatus.Conditions
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []certificatesv1.CertificateSigningRequestCondition) (errs field.ErrorList) {
// Uniqueness validation is implemented via custom, handwritten validation
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil

View file

@ -113,6 +113,7 @@ func Validate_CertificateSigningRequestStatus(ctx context.Context, op operation.
// field certificatesv1beta1.CertificateSigningRequestStatus.Conditions
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []certificatesv1beta1.CertificateSigningRequestCondition) (errs field.ErrorList) {
// Uniqueness validation is implemented via custom, handwritten validation
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil

View file

@ -177,6 +177,11 @@ func testValidateUpdateForDeclarative(t *testing.T, apiVersion string) {
),
subresources: []string{"/approval"}, // Can only modify Approved and Denied conditions on /approval subresource
},
"ratcheting: allow existing duplicate types - valid": {
old: makeValidCSR(withApprovedCondition(), withApprovedCondition(), withDeniedCondition(), withDeniedCondition()),
update: makeValidCSR(withDeniedCondition(), withDeniedCondition(), withApprovedCondition(), withApprovedCondition()),
subresources: []string{"/status"},
},
}
for k, tc := range testCases {

View file

@ -206,6 +206,7 @@ message CertificateSigningRequestStatus {
// +optional
// +k8s:listType=map
// +k8s:listMapKey=type
// +k8s:customUnique
// +k8s:optional
// +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember
// +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember

View file

@ -181,6 +181,7 @@ type CertificateSigningRequestStatus struct {
// +optional
// +k8s:listType=map
// +k8s:listMapKey=type
// +k8s:customUnique
// +k8s:optional
// +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember
// +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember

View file

@ -185,6 +185,7 @@ message CertificateSigningRequestStatus {
// +optional
// +k8s:listType=map
// +k8s:listMapKey=type
// +k8s:customUnique
// +k8s:optional
// +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember
// +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember

View file

@ -178,6 +178,7 @@ type CertificateSigningRequestStatus struct {
// +optional
// +k8s:listType=map
// +k8s:listMapKey=type
// +k8s:customUnique
// +k8s:optional
// +k8s:item(type: "Approved")=+k8s:zeroOrOneOfMember
// +k8s:item(type: "Denied")=+k8s:zeroOrOneOfMember

View file

@ -350,6 +350,8 @@ func Validate_T1(ctx context.Context, op operation.Operation, fldPath *field.Pat
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a other.StructType, b other.StructType) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *other.StructType) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, true, "field T1.SliceOfOtherStruct values")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a other.StructType, b other.StructType) bool { return a.StringField == b.StringField })...)
return
}(fldPath.Child("listMapOfOtherStruct"), obj.ListMapOfOtherStruct, safe.Field(oldObj, func(oldObj *T1) []other.StructType { return oldObj.ListMapOfOtherStruct }))...)

View file

@ -176,6 +176,8 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a ComparableStructWithKey, b ComparableStructWithKey) bool { return a.Key == b.Key }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ComparableStructWithKey) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSliceComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a ComparableStructWithKey, b ComparableStructWithKey) bool { return a.Key == b.Key })...)
return
}(fldPath.Child("mapSliceComparableField"), obj.MapSliceComparableField, safe.Field(oldObj, func(oldObj *StructSlice) []ComparableStructWithKey { return oldObj.MapSliceComparableField }))...)
@ -190,6 +192,8 @@ func Validate_StructSlice(ctx context.Context, op operation.Operation, fldPath *
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonComparableStructWithKey) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field MapSliceNonComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStructWithKey, b NonComparableStructWithKey) bool { return 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 NonComparableStructWithKey, b NonComparableStructWithKey) bool { return a.Key == b.Key }, validate.SemanticDeepEqual, Validate_NonComparableStructWithKey)...)
return

View file

@ -66,6 +66,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key1 == "b" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Item) field.ErrorList {
return validate.Subfield(ctx, op, fldPath, obj, oldObj, "stringField", func(o *Item) *string { return &o.StringField }, validate.DirectEqualPtr, validate.ImmutableByCompare)
})...)
// 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.Key1 == b.Key1 })...)
return
}(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.ListField }))...)

View file

@ -65,6 +65,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.StringKey == "target" && item.IntKey == 42 && item.BoolKey == true }, 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 Items[stringKey=target,intKey=42,boolKey=true]")
})...)
// 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.StringKey == b.StringKey && a.IntKey == b.IntKey && a.BoolKey == b.BoolKey
})...)
return
}(fldPath.Child("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...)
@ -79,6 +83,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.StringKey == "target" && item.IntKey == 42 && item.BoolKey == true }, 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 OutOfOrder[boolKey=42,stringKey=target,intKey=42]")
})...)
// 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.StringKey == b.StringKey && a.IntKey == b.IntKey && a.BoolKey == b.BoolKey
})...)
return
}(fldPath.Child("outOfOrder"), obj.OutOfOrder, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.OutOfOrder }))...)

View file

@ -73,6 +73,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
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 Items[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("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...)
@ -87,6 +89,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *IntKeyItem) bool { return item.IntField == 42 }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *IntKeyItem) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item IntKeyItems[intField=42]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a IntKeyItem, b IntKeyItem) bool { return a.IntField == b.IntField })...)
return
}(fldPath.Child("intKeyItems"), obj.IntKeyItems, safe.Field(oldObj, func(oldObj *Struct) []IntKeyItem { return oldObj.IntKeyItems }))...)
@ -101,6 +105,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *BoolKeyItem) bool { return item.BoolField == true }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *BoolKeyItem) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item BoolKeyItems[boolField=true]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a BoolKeyItem, b BoolKeyItem) bool { return a.BoolField == b.BoolField })...)
return
}(fldPath.Child("boolKeyItems"), obj.BoolKeyItems, safe.Field(oldObj, func(oldObj *Struct) []BoolKeyItem { return oldObj.BoolKeyItems }))...)
@ -115,6 +121,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *TypedefItem) bool { return item.ID == "typedef-target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *TypedefItem) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item TypedefItems[id=typedef-target]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a TypedefItem, b TypedefItem) bool { return a.ID == b.ID })...)
return
}(fldPath.Child("typedefItems"), obj.TypedefItems, safe.Field(oldObj, func(oldObj *Struct) TypedefItemList { return oldObj.TypedefItems }))...)
@ -153,6 +161,8 @@ func Validate_StructWithNestedTypedef(ctx context.Context, op operation.Operatio
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *NestedTypedefItem) bool { return item.Key == "nested-target" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NestedTypedefItem) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item NestedItems[key=nested-target]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NestedTypedefItem, b NestedTypedefItem) bool { return a.Key == b.Key })...)
return
}(fldPath.Child("nestedItems"), obj.NestedItems, safe.Field(oldObj, func(oldObj *StructWithNestedTypedef) []NestedTypedefItem { return oldObj.NestedItems }))...)

View file

@ -67,6 +67,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "item Items[key=target].stringField")
})
})...)
// 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("items"), obj.Items, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.Items }))...)
@ -83,6 +85,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
return validate.NEQ(ctx, op, fldPath, obj, oldObj, "forbidden")
})
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a RatchetItem, b RatchetItem) bool { return a.Key == b.Key })...)
return
}(fldPath.Child("ratchetItems"), obj.RatchetItems, safe.Field(oldObj, func(oldObj *Struct) []RatchetItem { return oldObj.RatchetItems }))...)

View file

@ -55,6 +55,8 @@ func Validate_ConflictingItemList(ctx context.Context, op operation.Operation, f
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")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a DualItem, b DualItem) bool { return a.ID == b.ID })...)
return errs
}
@ -65,6 +67,8 @@ func Validate_DualItemList(ctx context.Context, op operation.Operation, fldPath
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *DualItem) bool { return item.ID == "typedef-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 DualItems[id=typedef-target] from typedef")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a DualItem, b DualItem) bool { return a.ID == b.ID })...)
return errs
}
@ -76,6 +80,8 @@ func Validate_ItemList(ctx context.Context, op operation.Operation, fldPath *fie
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "validated" }, 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 ItemList[key=validated]")
})...)
// 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 errs
}
@ -86,6 +92,8 @@ func Validate_ItemListAlias(ctx context.Context, op operation.Operation, fldPath
errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *Item) bool { return item.Key == "aliased" }, 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 ItemListAlias[key=aliased]")
})...)
// 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 errs
}

View file

@ -64,6 +64,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
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 Task, b Task) bool { return a.Name == b.Name })...)
errs = append(errs, validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_union_simple_Struct_tasks_, func(list []Task) bool {
for i := range list {
if list[i].Name == "failed" {

View file

@ -74,6 +74,8 @@ var unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tag
// Validate_TaskList validates an instance of TaskList according
// to declarative validation rules in the API schema.
func Validate_TaskList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj TaskList) (errs field.ErrorList) {
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...)
errs = append(errs, validate.Union(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_union_typedef_TaskList_, func(list TaskList) bool {
for i := range list {
if list[i].Name == "failed" {

View file

@ -64,6 +64,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
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 Task, b Task) bool { return a.Name == b.Name })...)
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_zerorooneof_simple_Struct_tasks_, func(list []Task) bool {
for i := range list {
if list[i].Name == "failed" {

View file

@ -74,6 +74,8 @@ var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tes
// Validate_TaskList validates an instance of TaskList according
// to declarative validation rules in the API schema.
func Validate_TaskList(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj TaskList) (errs field.ErrorList) {
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a Task, b Task) bool { return a.Name == b.Name })...)
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_item_zerorooneof_typedef_TaskList_, func(list TaskList) bool {
for i := range list {
if list[i].Name == "failed" {

View file

@ -23,33 +23,29 @@ import (
)
func TestUniqueness(t *testing.T) {
// TODO: enable this once we have a way to either opt-out from this validation
// or settle the decision on how to handle the ratcheting cases.
/*
st := localSchemeBuilder.Test(t)
st := localSchemeBuilder.Test(t)
st.Value(&Struct{
ListField: []OtherStruct{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
ListTypedefField: []OtherTypedefStruct{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
TypedefField: ListType{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("listField").Index(2), nil),
field.Duplicate(field.NewPath("listTypedefField").Index(2), nil),
field.Duplicate(field.NewPath("typedefField").Index(2), nil),
})
*/
st.Value(&Struct{
ListField: []OtherStruct{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
ListTypedefField: []OtherTypedefStruct{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
TypedefField: ListType{
{"key1", 1, "one"}, // unique
{"key2", 2, "two"}, // dup
{"key2", 2, "two"},
},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("listField").Index(2), nil),
field.Duplicate(field.NewPath("listTypedefField").Index(2), nil),
field.Duplicate(field.NewPath("typedefField").Index(2), nil),
})
}
func TestUpdateCorrelation(t *testing.T) {

View file

@ -49,6 +49,17 @@ func RegisterValidations(scheme *testscheme.Scheme) error {
return nil
}
// Validate_ListType validates an instance of ListType according
// to declarative validation rules in the API schema.
func Validate_ListType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj ListType) (errs field.ErrorList) {
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
})...)
return errs
}
// 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) {
@ -65,6 +76,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
}, validate.DirectEqual, validate.ImmutableByCompare)...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
})...)
return
}(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListField }))...)
@ -79,6 +94,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
}, validate.DirectEqual, validate.ImmutableByCompare)...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
})...)
return
}(fldPath.Child("listTypedefField"), obj.ListTypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.ListTypedefField }))...)
@ -93,6 +112,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
}, validate.DirectEqual, validate.ImmutableByCompare)...)
// call the type's validation function
errs = append(errs, Validate_ListType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("typedefField"), obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) ListType { return oldObj.TypedefField }))...)
@ -109,6 +130,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
}, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
})...)
return
}(fldPath.Child("listComparableField"), obj.ListComparableField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListComparableField }))...)
@ -125,6 +150,10 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
}, validate.SemanticDeepEqual, 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 Struct.ListNonComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool {
return a.Key1Field == b.Key1Field && a.Key2Field == b.Key2Field
})...)
return
}(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...)

View file

@ -24,33 +24,29 @@ import (
)
func TestUniqueness(t *testing.T) {
// TODO: enable this once we have a way to either opt-out from this validation
// or settle the decision on how to handle the ratcheting cases.
/*
st := localSchemeBuilder.Test(t)
st := localSchemeBuilder.Test(t)
st.Value(&Struct{
ListField: []OtherStruct{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
ListTypedefField: []OtherTypedefStruct{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
TypedefField: ListType{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("listField").Index(2), nil),
field.Duplicate(field.NewPath("listTypedefField").Index(2), nil),
field.Duplicate(field.NewPath("typedefField").Index(2), nil),
})
*/
st.Value(&Struct{
ListField: []OtherStruct{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
ListTypedefField: []OtherTypedefStruct{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
TypedefField: ListType{
{"key1", "one"},
{"key2", "two"},
{"key2", "two"},
},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("listField").Index(2), nil),
field.Duplicate(field.NewPath("listTypedefField").Index(2), nil),
field.Duplicate(field.NewPath("typedefField").Index(2), nil),
})
}
func TestUpdateCorrelation(t *testing.T) {

View file

@ -49,6 +49,15 @@ func RegisterValidations(scheme *testscheme.Scheme) error {
return nil
}
// Validate_ListType validates an instance of ListType according
// to declarative validation rules in the API schema.
func Validate_ListType(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj ListType) (errs field.ErrorList) {
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...)
return errs
}
// 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) {
@ -63,6 +72,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
}
// call field-attached validations
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...)
return
}(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListField }))...)
@ -75,6 +86,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
}
// call field-attached validations
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherTypedefStruct, b OtherTypedefStruct) bool { return a.KeyField == b.KeyField })...)
return
}(fldPath.Child("listTypedefField"), obj.ListTypedefField, safe.Field(oldObj, func(oldObj *Struct) []OtherTypedefStruct { return oldObj.ListTypedefField }))...)
@ -87,6 +100,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
}
// call field-attached validations
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, validate.ImmutableByCompare)...)
// call the type's validation function
errs = append(errs, Validate_ListType(ctx, op, fldPath, obj, oldObj)...)
return
}(fldPath.Child("typedefField"), obj.TypedefField, safe.Field(oldObj, func(oldObj *Struct) ListType { return oldObj.TypedefField }))...)
@ -101,6 +116,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.KeyField == b.KeyField })...)
return
}(fldPath.Child("listComparableField"), obj.ListComparableField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListComparableField }))...)
@ -115,6 +132,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool { return a.KeyField == b.KeyField }, validate.SemanticDeepEqual, 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 Struct.ListNonComparableField[*]")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a NonComparableStruct, b NonComparableStruct) bool { return a.KeyField == b.KeyField })...)
return
}(fldPath.Child("listNonComparableField"), obj.ListNonComparableField, safe.Field(oldObj, func(oldObj *Struct) []NonComparableStruct { return oldObj.ListNonComparableField }))...)

View file

@ -218,6 +218,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListMapOfStructField vals")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField })...)
// iterate the list and call the type's validation function
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, Validate_OtherStruct)...)
return
@ -235,6 +237,8 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *OtherStruct) field.ErrorList {
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ListMapOfOpaqueStructField vals")
})...)
// lists with map semantics require unique keys
errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, func(a OtherStruct, b OtherStruct) bool { return a.StringField == b.StringField })...)
return
}(fldPath.Child("listMapOfOpaqueStructField"), obj.ListMapOfOpaqueStructField, safe.Field(oldObj, func(oldObj *Struct) []OtherStruct { return oldObj.ListMapOfOpaqueStructField }))...)

View file

@ -57,11 +57,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// optional value-type fields with zero-value defaults are purely documentation
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
return
}(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }))...)
@ -85,11 +80,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
// optional value-type fields with zero-value defaults are purely documentation
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
return
}(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...)
@ -113,11 +103,6 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) {
// optional value-type fields with zero-value defaults are purely documentation
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
return
}(fldPath.Child("boolField"), &obj.BoolField, safe.Field(oldObj, func(oldObj *Struct) *bool { return &oldObj.BoolField }))...)

View file

@ -49,6 +49,17 @@ type Struct struct {
// +k8s:unique=map
// +k8s:listMapKey=key
AtomicListUniqueMap []Item `json:"atomicListUniqueMap"`
// customUnique with listType=set
// +k8s:listType=set
// +k8s:customUnique
CustomUniqueListWithTypeSet []string `json:"customUniqueListWithTypeSet"`
// customUnique with listType=map
// +k8s:listType=map
// +k8s:listMapKey=key
// +k8s:customUnique
CustomUniqueListWithTypeMap []Item `json:"customUniqueListWithTypeMap"`
}
type Item struct {

View file

@ -43,6 +43,8 @@ func TestUnique(t *testing.T) {
{Key: "key1", Data: "one"},
{Key: "key2", Data: "two"},
},
CustomUniqueListWithTypeSet: []string{"a", "b", "a"},
CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}},
}).ExpectValid()
// Test empty lists
@ -51,6 +53,8 @@ func TestUnique(t *testing.T) {
SliceMapFieldWithMultipleKeys: []ItemWithMultipleKeys{},
AtomicListUniqueSet: []Item{},
AtomicListUniqueMap: []Item{},
CustomUniqueListWithTypeSet: []string{},
CustomUniqueListWithTypeMap: []Item{},
}).ExpectValid()
// Test single element lists
@ -59,6 +63,8 @@ func TestUnique(t *testing.T) {
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"}},
}).ExpectValid()
// Test duplicate values (should fail validation)
@ -79,6 +85,8 @@ func TestUnique(t *testing.T) {
{Key: "key2", Data: "two"},
{Key: "key1", Data: "three"},
},
CustomUniqueListWithTypeSet: []string{"a", "b", "a"},
CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(3), nil),
field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(4), nil),
@ -96,6 +104,8 @@ func TestUnique(t *testing.T) {
{Key: "a", Data: "two"},
{Key: "", Data: "three"},
},
CustomUniqueListWithTypeSet: []string{"", "a", ""},
CustomUniqueListWithTypeMap: []Item{{Key: ""}, {Key: "a"}, {Key: ""}},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Duplicate(field.NewPath("primitiveListUniqueSet").Index(2), nil),
field.Duplicate(field.NewPath("atomicListUniqueMap").Index(2), nil),
@ -119,6 +129,8 @@ func TestRatcheting(t *testing.T) {
{Key: "key1", Data: "one"},
{Key: "key2", Data: "two"},
},
CustomUniqueListWithTypeSet: []string{"a", "b", "a"},
CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "b"}, {Key: "a"}},
}
// Same data, different order.
@ -136,6 +148,8 @@ func TestRatcheting(t *testing.T) {
{Key: "key2", Data: "two"},
{Key: "key1", Data: "one"},
},
CustomUniqueListWithTypeSet: []string{"a", "a", "b"},
CustomUniqueListWithTypeMap: []Item{{Key: "a"}, {Key: "a"}, {Key: "b"}},
}
// Test that reordering doesn't trigger validation errors

View file

@ -106,5 +106,19 @@ func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field
return
}(fldPath.Child("atomicListUniqueMap"), obj.AtomicListUniqueMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.AtomicListUniqueMap }))...)
// field Struct.CustomUniqueListWithTypeSet
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []string) (errs field.ErrorList) {
// Uniqueness validation is implemented via custom, handwritten validation
return
}(fldPath.Child("customUniqueListWithTypeSet"), obj.CustomUniqueListWithTypeSet, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.CustomUniqueListWithTypeSet }))...)
// field Struct.CustomUniqueListWithTypeMap
errs = append(errs,
func(fldPath *field.Path, obj, oldObj []Item) (errs field.ErrorList) {
// Uniqueness validation is implemented via custom, handwritten validation
return
}(fldPath.Child("customUniqueListWithTypeMap"), obj.CustomUniqueListWithTypeMap, safe.Field(oldObj, func(oldObj *Struct) []Item { return oldObj.CustomUniqueListWithTypeMap }))...)
return errs
}

View file

@ -1105,10 +1105,12 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild
fldRatchetingChecked := false
if !validations.Empty() {
emitComments(validations.Comments, bufsw)
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
bufsw.Do("// call field-attached validations\n", nil)
emitCallsToValidators(c, validations.Functions, bufsw)
if len(validations.Functions) > 0 {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
bufsw.Do("// call field-attached validations\n", nil)
emitCallsToValidators(c, validations.Functions, bufsw)
}
}
// If the node is nil, this must be a type in a package we are not
@ -1132,11 +1134,14 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild
// validations, call its validation function.
if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() {
emitComments(validations.Comments, bufsw)
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
if len(validations.Functions) > 0 {
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
// Descend into this field.
g.emitValidationForChild(c, fld, bufsw)
@ -1145,21 +1150,25 @@ func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild
// validations, call its validation function.
if validations := fld.fieldKeyIterations; g.hasValidations(fld.node.key.node) && !validations.Empty() {
emitComments(validations.Comments, bufsw)
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
if len(validations.Functions) > 0 {
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
// If this field is a map and the value-type has
// validations, call its validation function.
if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() {
emitComments(validations.Comments, bufsw)
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
if len(validations.Functions) > 0 {
if !fldRatchetingChecked {
emitRatchetingCheck(c, fld.childType, bufsw)
fldRatchetingChecked = true
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
emitCallsToValidators(c, validations.Functions, bufsw)
}
// Descend into this field.
g.emitValidationForChild(c, fld, bufsw)

View file

@ -27,9 +27,10 @@ import (
)
const (
listTypeTagName = "k8s:listType"
ListMapKeyTagName = "k8s:listMapKey"
uniqueTagName = "k8s:unique"
listTypeTagName = "k8s:listType"
ListMapKeyTagName = "k8s:listMapKey"
uniqueTagName = "k8s:unique"
customUniqueTagName = "k8s:customUnique"
)
// globalListMeta is shared between list-related validators.
@ -40,6 +41,7 @@ func init() {
RegisterTagValidator(listTypeTagValidator{byPath: globalListMeta})
RegisterTagValidator(listMapKeyTagValidator{byPath: globalListMeta})
RegisterTagValidator(uniqueTagValidator{byPath: globalListMeta})
RegisterTagValidator(customUniqueTagValidator{byPath: globalListMeta})
// Finish work on the accumulated list metadata.
RegisterFieldValidator(listValidator{byPath: globalListMeta})
@ -70,6 +72,10 @@ type listMetadata struct {
semantic listSemantic
keyFields []string // 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.
customUnique bool
}
// makeListMapMatchFunc generates a function that compares two list-map
@ -137,16 +143,18 @@ func (lttv listTypeTagValidator) GetValidations(context Context, tag codetags.Ta
}
case "set":
lm.ownership = ownershipShared
// If uniqueTagValidator has run, lm.semantic will be non-empty and non-atomic.
// If uniqueTagValidator has run for `unique=set` or `unique=map`,
// lm.semantic will be non-empty and non-atomic.
if lm.semantic != "" && lm.semantic != semanticAtomic {
return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic")
return Validations{}, fmt.Errorf("unique tag is redundant for listType=%q", tag.Value)
}
lm.semantic = semanticSet
case "map":
lm.ownership = ownershipShared
// If uniqueTagValidator has run, lm.semantic will be non-empty and non-atomic.
// If uniqueTagValidator has run for `unique=set` or `unique=map`,
// lm.semantic will be non-empty and non-atomic.
if lm.semantic != "" && lm.semantic != semanticAtomic {
return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic")
return Validations{}, fmt.Errorf("unique tag is redundant for listType=%q", tag.Value)
}
if util.NativeType(t.Elem).Kind != types.Struct {
return Validations{}, fmt.Errorf("only lists of structs can be list-maps")
@ -267,7 +275,7 @@ func (utv uniqueTagValidator) GetValidations(context Context, tag codetags.Tag)
// If listType has already run and set a non-atomic ownership, this is an error.
if lm.ownership != "" && lm.ownership != ownershipSingle {
return Validations{}, fmt.Errorf("unique tag can only be used with listType=atomic")
return Validations{}, fmt.Errorf("unique tag may not be used with listType=set or listType=map")
}
if lm.semantic != "" && lm.semantic != semanticAtomic {
@ -306,6 +314,50 @@ func (utv uniqueTagValidator) Docs() TagDoc {
return doc
}
type customUniqueTagValidator struct {
byPath map[string]*listMetadata
}
func (customUniqueTagValidator) Init(Config) {}
func (customUniqueTagValidator) TagName() string {
return customUniqueTagName
}
func (customUniqueTagValidator) ValidScopes() sets.Set[Scope] {
return listTagsValidScopes
}
func (cutv customUniqueTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
// NOTE: pointers to lists are not supported, so we should never see a pointer here.
t := util.NativeType(context.Type)
if t.Kind != types.Slice && t.Kind != types.Array {
return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind)
}
lm := cutv.byPath[context.Path.String()]
if lm == nil {
lm = &listMetadata{}
cutv.byPath[context.Path.String()] = lm
}
lm.customUnique = true
// This tag doesn't generate any validations. It just accumulates
// information for other tags to use.
return Validations{}, nil
}
func (cutv customUniqueTagValidator) Docs() TagDoc {
doc := TagDoc{
Tag: cutv.TagName(),
Scopes: cutv.ValidScopes().UnsortedList(),
Description: "Indicates that uniqueness validation for this list is implemented via custom, handwritten validation. This disables generation of uniqueness validation for this list.",
Payloads: nil,
}
return doc
}
type listValidator struct {
byPath map[string]*listMetadata
}
@ -360,8 +412,13 @@ func (lv listValidator) GetValidations(context Context) (Validations, error) {
if err := lv.check(lm); err != nil {
return Validations{}, err
}
result := Validations{}
if lm.customUnique {
// Uniqueness validation is disabled in generated validation for this list.
// It would defer to handwritten validation to check the uniqueness.
result.AddComment("Uniqueness validation is implemented via custom, handwritten validation")
return result, nil
}
// Generate uniqueness checks for lists with higher-order semantics.
if lm.semantic == semanticSet {
@ -379,11 +436,7 @@ func (lv listValidator) GetValidations(context Context) (Validations, error) {
WithComment(comment)
result.AddFunction(f)
}
// TODO: Replace with the following once we have a way to either opt-out from
// uniqueness validation of list-maps or settle the decision on how to handle
// the ratcheting cases of it.
// if lm.semantic == semanticMap {
if lm.semantic == semanticMap && lm.ownership == ownershipSingle {
if lm.semantic == semanticMap {
// TODO: There are some fields which are declared as maps which do not
// enforce uniqueness in manual validation. Those either need to not be
// maps or we need to allow types to opt-out from this validation. SSA