Allow pointers for list keys

This commit is contained in:
Lalit Chauhan
2025-10-03 21:06:03 +00:00
parent 2158dd67e9
commit 4bd3d17e4a
20 changed files with 594 additions and 36 deletions

View File

@@ -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"`
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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",
},
})
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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",
},
})
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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),
})
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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")
}