add validate/zeroorone_test.go and add +k8s:zeroOrOneOfMember output tests

This commit is contained in:
Aaron Prindle
2025-07-16 22:00:31 +00:00
parent 10b20852e3
commit be72d963b8
10 changed files with 820 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validate
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestZeroOrOneOfUnion(t *testing.T) {
testCases := []struct {
name string
fields [][2]string
fieldValues []bool
expected field.ErrorList
}{
{
name: "one member set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, true},
expected: nil,
},
{
name: "two members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, true, false, true},
expected: field.ErrorList{field.Invalid(nil, "{b, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
},
{
name: "all members set",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{true, true, true, true},
expected: field.ErrorList{field.Invalid(nil, "{a, b, c, d}", "must specify at most one of: `a`, `b`, `c`, `d`").WithOrigin("zeroOrOneOf")},
},
{
name: "no member set - allowed for ZeroOrOneOf",
fields: [][2]string{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}},
fieldValues: []bool{false, false, false, false},
expected: nil, // This is the key difference from Union
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create mock extractors that return predefined values instead of
// actually extracting from the object.
extractors := make([]ExtractorFn[*testMember, bool], len(tc.fieldValues))
for i, val := range tc.fieldValues {
extractors[i] = func(_ *testMember) bool { return val }
}
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{}, nil, &testMember{}, nil, NewUnionMembership(tc.fields...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}
func TestZeroOrOneOfUnionRatcheting(t *testing.T) {
testCases := []struct {
name string
oldStruct *testStruct
newStruct *testStruct
expected field.ErrorList
}{
{
name: "both nil",
oldStruct: nil,
newStruct: nil,
},
{
name: "both empty struct - allowed for ZeroOrOneOf",
oldStruct: &testStruct{},
newStruct: &testStruct{},
},
{
name: "both have more than one member",
oldStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
},
{
name: "change to invalid",
oldStruct: &testStruct{
M1: &m1{},
},
newStruct: &testStruct{
M1: &m1{},
M2: &m2{},
},
expected: field.ErrorList{
field.Invalid(nil, "{m1, m2}", "must specify at most one of: `m1`, `m2`").WithOrigin("zeroOrOneOf"),
},
},
{
name: "change from empty to one member - allowed",
oldStruct: &testStruct{},
newStruct: &testStruct{
M1: &m1{},
},
expected: nil,
},
{
name: "change from one member to empty - allowed",
oldStruct: &testStruct{
M1: &m1{},
},
newStruct: &testStruct{},
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := ZeroOrOneOfUnion(context.Background(), operation.Operation{Type: operation.Update}, nil, tc.newStruct, tc.oldStruct, NewUnionMembership([][2]string{{"m1", "m1"}, {"m2", "m2"}}...), extractors...)
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}

View File

@@ -0,0 +1,44 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package custommembers
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
// Non-discriminated zero-or-one-of union with custom member names
type Struct struct {
TypeMeta int
NonUnionField string `json:"nonUnionField"`
// +k8s:zeroOrOneOfMember(memberName: "CustomM1")
// +k8s:optional
M1 *M1 `json:"m1"`
// +k8s:zeroOrOneOfMember(memberName: "CustomM2")
// +k8s:optional
M2 *M2 `json:"m2"`
}
type M1 struct{}
type M2 struct{}

View File

@@ -0,0 +1,41 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package custommembers
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func Test(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Empty union is valid
st.Value(&Struct{}).ExpectValid()
st.Value(&Struct{M1: &M1{}}).ExpectValid()
st.Value(&Struct{M2: &M2{}}).ExpectValid()
st.Value(&Struct{M1: &M1{}, M2: &M2{}}).ExpectInvalid(
field.Invalid(nil, "{m1, m2}", "must specify at most one of: `m1`, `m2`"),
)
// Test validation ratcheting
st.Value(&Struct{M1: &M1{}, M2: &M2{}}).OldValue(&Struct{M1: &M1{}, M2: &M2{}}).ExpectValid()
st.Value(&Struct{}).OldValue(&Struct{}).ExpectValid()
}

View File

@@ -0,0 +1,98 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by validation-gen. DO NOT EDIT.
package custommembers
import (
context "context"
fmt "fmt"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
field "k8s.io/apimachinery/pkg/util/validation/field"
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *testscheme.Scheme) error {
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_custom_members_Struct_ = validate.NewUnionMembership([2]string{"m1", "CustomM1"}, [2]string{"m2", "CustomM2"})
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
// type Struct
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil // no changes
}
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_custom_members_Struct_, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.M1 != nil
}, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.M2 != nil
})...)
// field Struct.TypeMeta has no validation
// field Struct.NonUnionField has no validation
// field Struct.M1
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M1) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m1"), obj.M1, safe.Field(oldObj, func(oldObj *Struct) *M1 { return oldObj.M1 }))...)
// field Struct.M2
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M2) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m2"), obj.M2, safe.Field(oldObj, func(oldObj *Struct) *M2 { return oldObj.M2 }))...)
return errs
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package multiple
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
// Two non-discriminated zero-or-one-of unions in the same struct
type Struct struct {
TypeMeta int
NonUnionField string `json:"nonUnionField"`
// +k8s:zeroOrOneOfMember(union: "union1")
// +k8s:optional
U1M1 *M1 `json:"u1m1"`
// +k8s:zeroOrOneOfMember(union: "union1")
// +k8s:optional
U1M2 *M2 `json:"u1m2"`
// +k8s:zeroOrOneOfMember(union: "union2")
// +k8s:optional
U2M1 *M1 `json:"u2m1"`
// +k8s:zeroOrOneOfMember(union: "union2")
// +k8s:optional
U2M2 *M2 `json:"u2m2"`
}
type M1 struct{}
type M2 struct{}

View File

@@ -0,0 +1,67 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multiple
import (
"testing"
field "k8s.io/apimachinery/pkg/util/validation/field"
)
func Test(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Both unions can be empty
st.Value(&Struct{}).ExpectValid()
// One member from each union
st.Value(&Struct{U1M1: &M1{}, U2M1: &M1{}}).ExpectValid()
st.Value(&Struct{U1M2: &M2{}, U2M2: &M2{}}).ExpectValid()
// One union with member, other empty
st.Value(&Struct{U1M1: &M1{}}).ExpectValid()
st.Value(&Struct{U2M2: &M2{}}).ExpectValid()
// Multiple members in one union
st.Value(&Struct{U1M1: &M1{}, U1M2: &M2{}}).ExpectInvalid(
field.Invalid(nil, "{u1m1, u1m2}", "must specify at most one of: `u1m1`, `u1m2`"),
)
st.Value(&Struct{U2M1: &M1{}, U2M2: &M2{}}).ExpectInvalid(
field.Invalid(nil, "{u2m1, u2m2}", "must specify at most one of: `u2m1`, `u2m2`"),
)
st.Value(&Struct{
U1M1: &M1{}, U1M2: &M2{},
U2M1: &M1{}, U2M2: &M2{},
}).ExpectInvalid(
field.Invalid(nil, "{u1m1, u1m2}", "must specify at most one of: `u1m1`, `u1m2`"),
field.Invalid(nil, "{u2m1, u2m2}", "must specify at most one of: `u2m1`, `u2m2`"),
)
// Test validation ratcheting
st.Value(&Struct{}).OldValue(&Struct{}).ExpectValid()
st.Value(&Struct{U1M1: &M1{}, U1M2: &M2{}}).OldValue(&Struct{U1M1: &M1{}, U1M2: &M2{}}).ExpectValid()
st.Value(&Struct{U2M1: &M1{}, U2M2: &M2{}}).OldValue(&Struct{U2M1: &M1{}, U2M2: &M2{}}).ExpectValid()
st.Value(&Struct{
U1M1: &M1{}, U1M2: &M2{},
U2M1: &M1{}, U2M2: &M2{},
}).OldValue(&Struct{
U1M1: &M1{}, U1M2: &M2{},
U2M1: &M1{}, U2M2: &M2{},
}).ExpectValid()
}

View File

@@ -0,0 +1,134 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by validation-gen. DO NOT EDIT.
package multiple
import (
context "context"
fmt "fmt"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
field "k8s.io/apimachinery/pkg/util/validation/field"
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *testscheme.Scheme) error {
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_multiple_Struct_union1 = validate.NewUnionMembership([2]string{"u1m1", "U1M1"}, [2]string{"u1m2", "U1M2"})
var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_multiple_Struct_union2 = validate.NewUnionMembership([2]string{"u2m1", "U2M1"}, [2]string{"u2m2", "U2M2"})
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
// type Struct
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil // no changes
}
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_multiple_Struct_union1, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.U1M1 != nil
}, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.U1M2 != nil
})...)
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_multiple_Struct_union2, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.U2M1 != nil
}, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.U2M2 != nil
})...)
// field Struct.TypeMeta has no validation
// field Struct.NonUnionField has no validation
// field Struct.U1M1
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M1) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("u1m1"), obj.U1M1, safe.Field(oldObj, func(oldObj *Struct) *M1 { return oldObj.U1M1 }))...)
// field Struct.U1M2
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M2) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("u1m2"), obj.U1M2, safe.Field(oldObj, func(oldObj *Struct) *M2 { return oldObj.U1M2 }))...)
// field Struct.U2M1
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M1) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("u2m1"), obj.U2M1, safe.Field(oldObj, func(oldObj *Struct) *M1 { return oldObj.U2M1 }))...)
// field Struct.U2M2
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M2) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("u2m2"), obj.U2M2, safe.Field(oldObj, func(oldObj *Struct) *M2 { return oldObj.U2M2 }))...)
return errs
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package simple
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
// Non-discriminated zero-or-one-of union
type Struct struct {
TypeMeta int
NonUnionField string `json:"nonUnionField"`
// +k8s:zeroOrOneOfMember
// +k8s:optional
M1 *M1 `json:"m1"`
// +k8s:zeroOrOneOfMember
// +k8s:optional
M2 *M2 `json:"m2"`
// +k8s:zeroOrOneOfMember
// +k8s:optional
M3 string `json:"m3"`
// +k8s:zeroOrOneOfMember
// +k8s:optional
M4 *string `json:"m4"`
}
type M1 struct{}
type M2 struct{}

View File

@@ -0,0 +1,54 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package simple
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func Test(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Empty union is valid for zeroOrOneOf
st.Value(&Struct{}).ExpectValid()
st.Value(&Struct{M1: &M1{}}).ExpectValid()
st.Value(&Struct{M2: &M2{}}).ExpectValid()
st.Value(&Struct{M3: "a string"}).ExpectValid()
st.Value(&Struct{M4: ptr.To("a string")}).ExpectValid()
st.Value(&Struct{M1: &M1{}, M2: &M2{}}).ExpectInvalid(
field.Invalid(nil, "{m1, m2}", "must specify at most one of: `m1`, `m2`, `m3`, `m4`"),
)
st.Value(&Struct{M1: &M1{}, M3: "a string"}).ExpectInvalid(
field.Invalid(nil, "{m1, m3}", "must specify at most one of: `m1`, `m2`, `m3`, `m4`"),
)
st.Value(&Struct{M1: &M1{}, M4: ptr.To("a string")}).ExpectInvalid(
field.Invalid(nil, "{m1, m4}", "must specify at most one of: `m1`, `m2`, `m3`, `m4`"),
)
// Update only considers whether a field was set, not the value.
st.Value(&Struct{M3: "a string"}).OldValue(&Struct{M3: "different string"}).ExpectValid()
// Test validation ratcheting
st.Value(&Struct{}).OldValue(&Struct{}).ExpectValid()
st.Value(&Struct{M1: &M1{}, M2: &M2{}}).OldValue(&Struct{M1: &M1{}, M2: &M2{}}).ExpectValid()
st.Value(&Struct{M3: "a string", M2: &M2{}}).OldValue(&Struct{M3: "different string", M2: &M2{}}).ExpectValid()
}

View File

@@ -0,0 +1,133 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by validation-gen. DO NOT EDIT.
package simple
import (
context "context"
fmt "fmt"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
field "k8s.io/apimachinery/pkg/util/validation/field"
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *testscheme.Scheme) error {
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_simple_Struct_ = validate.NewUnionMembership([2]string{"m1", "M1"}, [2]string{"m2", "M2"}, [2]string{"m3", "M3"}, [2]string{"m4", "M4"})
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
// type Struct
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil // no changes
}
errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_zerooroneof_zerooroneof_simple_Struct_, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.M1 != nil
}, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.M2 != nil
}, func(obj *Struct) bool {
if obj == nil {
return false
}
var z string
return obj.M3 != z
}, func(obj *Struct) bool {
if obj == nil {
return false
}
return obj.M4 != nil
})...)
// field Struct.TypeMeta has no validation
// field Struct.NonUnionField has no validation
// field Struct.M1
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M1) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m1"), obj.M1, safe.Field(oldObj, func(oldObj *Struct) *M1 { return oldObj.M1 }))...)
// field Struct.M2
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *M2) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m2"), obj.M2, safe.Field(oldObj, func(oldObj *Struct) *M2 { return oldObj.M2 }))...)
// field Struct.M3
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m3"), &obj.M3, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.M3 }))...)
// field Struct.M4
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil // no changes
}
if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
return // do not proceed
}
return
}(fldPath.Child("m4"), obj.M4, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.M4 }))...)
return errs
}