diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validate/zeroorone_test.go b/staging/src/k8s.io/apimachinery/pkg/api/validate/zeroorone_test.go new file mode 100644 index 00000000000..b9264be050f --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/validate/zeroorone_test.go @@ -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) + } + }) + } +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc.go new file mode 100644 index 00000000000..d1c1318ab91 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc.go @@ -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{} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc_test.go new file mode 100644 index 00000000000..9707ab935c4 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/doc_test.go @@ -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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/zz_generated.validations.go new file mode 100644 index 00000000000..42838db0f84 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/custom_members/zz_generated.validations.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc.go new file mode 100644 index 00000000000..594441b1583 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc.go @@ -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{} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc_test.go new file mode 100644 index 00000000000..d1f1155149f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/doc_test.go @@ -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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/zz_generated.validations.go new file mode 100644 index 00000000000..453e46b41c6 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/multiple/zz_generated.validations.go @@ -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 +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc.go new file mode 100644 index 00000000000..d6d85915420 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc.go @@ -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{} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc_test.go new file mode 100644 index 00000000000..f2024c936c1 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/doc_test.go @@ -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() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/zz_generated.validations.go new file mode 100644 index 00000000000..b93470cc83a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/zerooroneof/zerooroneof/simple/zz_generated.validations.go @@ -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 +}