mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
Allow pointers for list keys
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user