From 90578014e64c3271dff53c23a177cf4751733b01 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 5 Feb 2026 06:03:53 +0000 Subject: [PATCH] Add alpha and beta tests --- .../tags/levels/atomicslice/doc.go | 53 +++ .../tags/levels/atomicslice/doc_test.go | 50 +++ .../atomicslice/zz_generated.validations.go | 128 ++++++ .../output_tests/tags/levels/enums/doc.go | 46 +++ .../tags/levels/enums/doc_test.go | 57 +++ .../levels/enums/zz_generated.validations.go | 102 +++++ .../output_tests/tags/levels/listkeys/doc.go | 117 ++++++ .../tags/levels/listkeys/doc_test.go | 147 +++++++ .../listkeys/zz_generated.validations.go | 240 ++++++++++++ .../tags/levels/listmapitem/doc.go | 79 ++++ .../tags/levels/listmapitem/doc_test.go | 59 +++ .../listmapitem/zz_generated.validations.go | 198 ++++++++++ .../output_tests/tags/levels/listset/doc.go | 71 ++++ .../tags/levels/listset/doc_test.go | 70 ++++ .../listset/zz_generated.validations.go | 192 +++++++++ .../tags/levels/mapvalidation/doc.go | 60 +++ .../tags/levels/mapvalidation/doc_test.go | 62 +++ .../mapvalidation/zz_generated.validations.go | 170 ++++++++ .../tags/levels/optionalrequired/doc.go | 34 ++ .../tags/levels/optionalrequired/doc_test.go | 64 +++ .../zz_generated.validations.go | 95 +++++ .../output_tests/tags/levels/simple/doc.go | 90 +++++ .../tags/levels/simple/doc_test.go | 140 +++++++ .../levels/simple/zz_generated.validations.go | 368 ++++++++++++++++++ .../output_tests/tags/levels/structs/doc.go | 60 +++ .../tags/levels/structs/doc_test.go | 92 +++++ .../structs/zz_generated.validations.go | 198 ++++++++++ .../output_tests/tags/levels/subfields/doc.go | 38 ++ .../tags/levels/subfields/doc_test.go | 57 +++ .../subfields/zz_generated.validations.go | 89 +++++ .../output_tests/tags/levels/unions/doc.go | 104 +++++ .../tags/levels/unions/doc_test.go | 73 ++++ .../levels/unions/zz_generated.validations.go | 317 +++++++++++++++ .../output_tests/tags/levels/uniquetag/doc.go | 36 ++ .../tags/levels/uniquetag/doc_test.go | 35 ++ .../uniquetag/zz_generated.validations.go | 84 ++++ 36 files changed, 3875 insertions(+) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/zz_generated.validations.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc_test.go create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/zz_generated.validations.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc.go new file mode 100644 index 00000000000..27c902385f1 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc.go @@ -0,0 +1,53 @@ +/* +Copyright 2025 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 + +package atomicslice + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type AtomicSliceStruct struct { + TypeMeta int + + // Case: Standard eachVal + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:minimum=10 + Standard []int `json:"standard"` + + // Case: Alpha eachVal + // +k8s:listType=atomic + // +k8s:alpha=+k8s:eachVal=+k8s:minimum=10 + Alpha []int `json:"Alpha"` + + // Case: Beta eachVal + // +k8s:listType=atomic + // +k8s:beta=+k8s:eachVal=+k8s:minimum=10 + Beta []int `json:"Beta"` + + // Case: Standard eachVal, Alpha validation + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:alpha=+k8s:minimum=10 + AlphaValidation []int `json:"AlphaValidation"` + + // Case: Standard eachVal, Beta validation + // +k8s:listType=atomic + // +k8s:eachVal=+k8s:beta=+k8s:minimum=10 + BetaValidation []int `json:"BetaValidation"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc_test.go new file mode 100644 index 00000000000..3193ec10cad --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/doc_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2025 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 atomicslice + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestAtomicSlice(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&AtomicSliceStruct{ + Standard: []int{5}, + Alpha: []int{5}, + Beta: []int{5}, + AlphaValidation: []int{5}, + BetaValidation: []int{5}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + // Case: Standard -> Normal Error + field.Invalid(field.NewPath("standard").Index(0), 5, "").WithOrigin("minimum"), + + // Case: Alpha eachVal -> Alpha Error + field.Invalid(field.NewPath("Alpha").Index(0), 5, "").WithOrigin("minimum").MarkAlpha(), + + // Case: Beta eachVal -> Beta Error + field.Invalid(field.NewPath("Beta").Index(0), 5, "").WithOrigin("minimum").MarkBeta(), + + // Case: Standard eachVal, Alpha validation -> Alpha Error + field.Invalid(field.NewPath("AlphaValidation").Index(0), 5, "").WithOrigin("minimum").MarkAlpha(), + + // Case: Standard eachVal, Beta validation -> Beta Error + field.Invalid(field.NewPath("BetaValidation").Index(0), 5, "").WithOrigin("minimum").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/zz_generated.validations.go new file mode 100644 index 00000000000..b9590a417f2 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/atomicslice/zz_generated.validations.go @@ -0,0 +1,128 @@ +//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 atomicslice + +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 { + // type AtomicSliceStruct + scheme.AddValidationFunc((*AtomicSliceStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_AtomicSliceStruct(ctx, op, nil /* fldPath */, obj.(*AtomicSliceStruct), safe.Cast[*AtomicSliceStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_AtomicSliceStruct validates an instance of AtomicSliceStruct according +// to declarative validation rules in the API schema. +func Validate_AtomicSliceStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *AtomicSliceStruct) (errs field.ErrorList) { + // field AtomicSliceStruct.TypeMeta has no validation + + // field AtomicSliceStruct.Standard + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, nil, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + })...) + return + }(fldPath.Child("standard"), obj.Standard, safe.Field(oldObj, func(oldObj *AtomicSliceStruct) []int { return oldObj.Standard }), oldObj != nil)...) + + // field AtomicSliceStruct.Alpha + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, nil, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }).MarkAlpha()...) + return + }(fldPath.Child("Alpha"), obj.Alpha, safe.Field(oldObj, func(oldObj *AtomicSliceStruct) []int { return oldObj.Alpha }), oldObj != nil)...) + + // field AtomicSliceStruct.Beta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, nil, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }).MarkBeta()...) + return + }(fldPath.Child("Beta"), obj.Beta, safe.Field(oldObj, func(oldObj *AtomicSliceStruct) []int { return oldObj.Beta }), oldObj != nil)...) + + // field AtomicSliceStruct.AlphaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, nil, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha() + })...) + return + }(fldPath.Child("AlphaValidation"), obj.AlphaValidation, safe.Field(oldObj, func(oldObj *AtomicSliceStruct) []int { return oldObj.AlphaValidation }), oldObj != nil)...) + + // field AtomicSliceStruct.BetaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, nil, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta() + })...) + return + }(fldPath.Child("BetaValidation"), obj.BetaValidation, safe.Field(oldObj, func(oldObj *AtomicSliceStruct) []int { return oldObj.BetaValidation }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc.go new file mode 100644 index 00000000000..400b501dbc5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc.go @@ -0,0 +1,46 @@ +/* +Copyright 2025 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 + +package enums + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + EnumField Enum `json:"enumField"` + + EnumFieldBeta BetaEnum `json:"enumFieldBeta"` +} + +// +k8s:alpha=+k8s:enum +type Enum string + +const ( + EnumA Enum = "A" +) + +// +k8s:beta=+k8s:enum +type BetaEnum string + +const ( + BetaEnumA BetaEnum = "A" +) diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc_test.go new file mode 100644 index 00000000000..3a2fca97428 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/doc_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 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 enums + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + EnumField: "A", + EnumFieldBeta: "A", + }).ExpectValid() + + // Test failures marked as alpha + st.Value(&Struct{ + EnumField: "B", + EnumFieldBeta: "A", + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.NotSupported(field.NewPath("enumField"), Enum("B"), []string{"A"}).MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + EnumField: "A", + EnumFieldBeta: "A", + }).ExpectValid() + + // Test failures marked as beta + st.Value(&Struct{ + EnumField: "A", + EnumFieldBeta: "B", + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.NotSupported(field.NewPath("enumFieldBeta"), BetaEnum("B"), []string{"A"}).MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/zz_generated.validations.go new file mode 100644 index 00000000000..936fe4998dd --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/enums/zz_generated.validations.go @@ -0,0 +1,102 @@ +//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 enums + +import ( + context "context" + fmt "fmt" + + operation "k8s.io/apimachinery/pkg/api/operation" + safe "k8s.io/apimachinery/pkg/api/safe" + validate "k8s.io/apimachinery/pkg/api/validate" + sets "k8s.io/apimachinery/pkg/util/sets" + 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 { + // type Struct + 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 symbolsForBetaEnum = sets.New(BetaEnumA) + +// Validate_BetaEnum validates an instance of BetaEnum according +// to declarative validation rules in the API schema. +func Validate_BetaEnum(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *BetaEnum) (errs field.ErrorList) { + errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForBetaEnum, nil).MarkBeta()...) + + return errs +} + +var symbolsForEnum = sets.New(EnumA) + +// Validate_Enum validates an instance of Enum according +// to declarative validation rules in the API schema. +func Validate_Enum(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum) (errs field.ErrorList) { + errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum, nil).MarkAlpha()...) + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.EnumField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Enum, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_Enum(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("enumField"), &obj.EnumField, safe.Field(oldObj, func(oldObj *Struct) *Enum { return &oldObj.EnumField }), oldObj != nil)...) + + // field Struct.EnumFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *BetaEnum, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call the type's validation function + errs = append(errs, Validate_BetaEnum(ctx, op, fldPath, obj, oldObj)...) + return + }(fldPath.Child("enumFieldBeta"), &obj.EnumFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *BetaEnum { return &oldObj.EnumFieldBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc.go new file mode 100644 index 00000000000..7db959bb7bf --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc.go @@ -0,0 +1,117 @@ +/* +Copyright 2025 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 + +package listkeys + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type MapItem struct { + Key string `json:"key"` + Value int `json:"value"` +} + +type MultiKeyItem struct { + Key1 string `json:"key1"` + Key2 int `json:"key2"` + Value int `json:"value"` +} + +type ListKeyStruct struct { + TypeMeta int + + // Case: Alpha listType, Standard Key + // +k8s:alpha=+k8s:listType=map + // +k8s:listMapKey=key + AlphaListTypeStandardKey []MapItem `json:"alphaListTypeStandardKey"` + + // Case: Standard listType, Alpha Key + // +k8s:listType=map + // +k8s:alpha=+k8s:listMapKey=key + StandardListTypeAlphaKey []MapItem `json:"standardListTypeAlphaKey"` + + // Case: Alpha listType, Alpha Key + // +k8s:alpha=+k8s:listType=map + // +k8s:alpha=+k8s:listMapKey=key + AlphaListTypeAlphaKey []MapItem `json:"alphaListTypeAlphaKey"` + + // Case: Standard listType, Alpha Key1, Standard Key2 + // +k8s:listType=map + // +k8s:alpha=+k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + StandardListTypeMixedKeys1 []MultiKeyItem `json:"standardListTypeMixedKeys1"` + + // Case: Standard listType, Standard Key1, Alpha Key2 + // +k8s:listType=map + // +k8s:listMapKey=key1 + // +k8s:alpha=+k8s:listMapKey=key2 + StandardListTypeMixedKeys2 []MultiKeyItem `json:"standardListTypeMixedKeys2"` + + // Case: Alpha listType, Alpha Key1, Standard Key2 + // +k8s:alpha=+k8s:listType=map + // +k8s:alpha=+k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + AlphaListTypeMixedKeys1 []MultiKeyItem `json:"alphaListTypeMixedKeys1"` + + // Case: Alpha listType, Standard Key1, Alpha Key2 + // +k8s:alpha=+k8s:listType=map + // +k8s:listMapKey=key1 + // +k8s:alpha=+k8s:listMapKey=key2 + AlphaListTypeMixedKeys2 []MultiKeyItem `json:"alphaListTypeMixedKeys2"` + + // Case: Standard listType, Beta Key1, Standard Key2 + // +k8s:listType=map + // +k8s:beta=+k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + StandardListTypeMixedKeysBeta1 []MultiKeyItem `json:"standardListTypeMixedKeysBeta1"` + + // Case: Standard listType, Standard Key1, Beta Key2 + // +k8s:listType=map + // +k8s:listMapKey=key1 + // +k8s:beta=+k8s:listMapKey=key2 + StandardListTypeMixedKeysBeta2 []MultiKeyItem `json:"standardListTypeMixedKeysBeta2"` + + // Case: Beta listType, Beta Key1, Standard Key2 + // +k8s:beta=+k8s:listType=map + // +k8s:beta=+k8s:listMapKey=key1 + // +k8s:listMapKey=key2 + BetaListTypeMixedKeys1 []MultiKeyItem `json:"betaListTypeMixedKeys1"` + + // Case: Beta listType, Standard Key1, Beta Key2 + // +k8s:beta=+k8s:listType=map + // +k8s:listMapKey=key1 + // +k8s:beta=+k8s:listMapKey=key2 + BetaListTypeMixedKeys2 []MultiKeyItem `json:"betaListTypeMixedKeys2"` + + // Case: Beta listType, Standard Key + // +k8s:beta=+k8s:listType=map + // +k8s:listMapKey=key + BetaListTypeStandardKey []MapItem `json:"betaListTypeStandardKey"` + + // Case: Standard listType, Beta Key + // +k8s:listType=map + // +k8s:beta=+k8s:listMapKey=key + StandardListTypeBetaKey []MapItem `json:"standardListTypeBetaKey"` + + // Case: Beta listType, Beta Key + // +k8s:beta=+k8s:listType=map + // +k8s:beta=+k8s:listMapKey=key + BetaListTypeBetaKey []MapItem `json:"betaListTypeBetaKey"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc_test.go new file mode 100644 index 00000000000..b18ccd769c5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/doc_test.go @@ -0,0 +1,147 @@ +/* +Copyright 2025 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 listkeys + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestListKeys(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&ListKeyStruct{ + // Case 1: Alpha listType, Standard Key + AlphaListTypeStandardKey: []MapItem{ + {Key: "a", Value: 1}, + {Key: "a", Value: 2}, + }, + // Case 2: Standard listType, Alpha Key + StandardListTypeAlphaKey: []MapItem{ + {Key: "b", Value: 1}, + {Key: "b", Value: 2}, + }, + // Case 3: Alpha listType, Alpha Key + AlphaListTypeAlphaKey: []MapItem{ + {Key: "c", Value: 1}, + {Key: "c", Value: 2}, + }, + // Case 4: Standard listType, Alpha Key1, Standard Key2 + StandardListTypeMixedKeys1: []MultiKeyItem{ + {Key1: "d", Key2: 1, Value: 1}, + {Key1: "d", Key2: 1, Value: 2}, + }, + // Case 5: Standard listType, Standard Key1, Alpha Key2 + StandardListTypeMixedKeys2: []MultiKeyItem{ + {Key1: "e", Key2: 1, Value: 1}, + {Key1: "e", Key2: 1, Value: 2}, + }, + // Case 6: Alpha listType, Alpha Key1, Standard Key2 + AlphaListTypeMixedKeys1: []MultiKeyItem{ + {Key1: "f", Key2: 1, Value: 1}, + {Key1: "f", Key2: 1, Value: 2}, + }, + // Case 7: Alpha listType, Standard Key1, Alpha Key2 + AlphaListTypeMixedKeys2: []MultiKeyItem{ + {Key1: "g", Key2: 1, Value: 1}, + {Key1: "g", Key2: 1, Value: 2}, + }, + + // Case 11: Standard listType, Beta Key1, Standard Key2 + StandardListTypeMixedKeysBeta1: []MultiKeyItem{ + {Key1: "k", Key2: 1, Value: 1}, + {Key1: "k", Key2: 1, Value: 2}, + }, + // Case 12: Standard listType, Standard Key1, Beta Key2 + StandardListTypeMixedKeysBeta2: []MultiKeyItem{ + {Key1: "l", Key2: 1, Value: 1}, + {Key1: "l", Key2: 1, Value: 2}, + }, + // Case 13: Beta listType, Beta Key1, Standard Key2 + BetaListTypeMixedKeys1: []MultiKeyItem{ + {Key1: "m", Key2: 1, Value: 1}, + {Key1: "m", Key2: 1, Value: 2}, + }, + // Case 14: Beta listType, Standard Key1, Beta Key2 + BetaListTypeMixedKeys2: []MultiKeyItem{ + {Key1: "n", Key2: 1, Value: 1}, + {Key1: "n", Key2: 1, Value: 2}, + }, + + // Case 8: Beta listType, Standard Key + BetaListTypeStandardKey: []MapItem{ + {Key: "h", Value: 1}, + {Key: "h", Value: 2}, + }, + // Case 9: Standard listType, Beta Key + StandardListTypeBetaKey: []MapItem{ + {Key: "i", Value: 1}, + {Key: "i", Value: 2}, + }, + // Case 10: Beta listType, Beta Key + BetaListTypeBetaKey: []MapItem{ + {Key: "j", Value: 1}, + {Key: "j", Value: 2}, + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + // Case 1: Alpha listType, Standard Key -> Alpha Error + field.Duplicate(field.NewPath("alphaListTypeStandardKey").Index(1), MapItem{Key: "a", Value: 2}).MarkAlpha(), + + // Case 2: Standard listType, Alpha Key -> Normal Error + // The alpha key is treated as a functional key for the list validation. + field.Duplicate(field.NewPath("standardListTypeAlphaKey").Index(1), MapItem{Key: "b", Value: 2}), + + // Case 3: Alpha listType, Alpha Key -> Alpha Error + field.Duplicate(field.NewPath("alphaListTypeAlphaKey").Index(1), MapItem{Key: "c", Value: 2}).MarkAlpha(), + + // Case 4: Standard listType, Alpha Key1, Standard Key2 -> Normal Error + // Both keys participate in the uniqueness check. + field.Duplicate(field.NewPath("standardListTypeMixedKeys1").Index(1), MultiKeyItem{Key1: "d", Key2: 1, Value: 2}), + + // Case 5: Standard listType, Standard Key1, Alpha Key2 -> Normal Error + field.Duplicate(field.NewPath("standardListTypeMixedKeys2").Index(1), MultiKeyItem{Key1: "e", Key2: 1, Value: 2}), + + // Case 6: Alpha listType, Alpha Key1, Standard Key2 -> Alpha Error + field.Duplicate(field.NewPath("alphaListTypeMixedKeys1").Index(1), MultiKeyItem{Key1: "f", Key2: 1, Value: 2}).MarkAlpha(), + + // Case 7: Alpha listType, Standard Key1, Alpha Key2 -> Alpha Error + field.Duplicate(field.NewPath("alphaListTypeMixedKeys2").Index(1), MultiKeyItem{Key1: "g", Key2: 1, Value: 2}).MarkAlpha(), + + // Case 11: Standard listType, Beta Key1, Standard Key2 -> Normal Error + field.Duplicate(field.NewPath("standardListTypeMixedKeysBeta1").Index(1), MultiKeyItem{Key1: "k", Key2: 1, Value: 2}), + + // Case 12: Standard listType, Standard Key1, Beta Key2 -> Normal Error + field.Duplicate(field.NewPath("standardListTypeMixedKeysBeta2").Index(1), MultiKeyItem{Key1: "l", Key2: 1, Value: 2}), + + // Case 13: Beta listType, Beta Key1, Standard Key2 -> Beta Error + field.Duplicate(field.NewPath("betaListTypeMixedKeys1").Index(1), MultiKeyItem{Key1: "m", Key2: 1, Value: 2}).MarkBeta(), + + // Case 14: Beta listType, Standard Key1, Beta Key2 -> Beta Error + field.Duplicate(field.NewPath("betaListTypeMixedKeys2").Index(1), MultiKeyItem{Key1: "n", Key2: 1, Value: 2}).MarkBeta(), + + // Case 8: Beta listType, Standard Key -> Beta Error + field.Duplicate(field.NewPath("betaListTypeStandardKey").Index(1), MapItem{Key: "h", Value: 2}).MarkBeta(), + + // Case 9: Standard listType, Beta Key -> Normal Error + // The beta key is treated as a functional key for the list validation. + field.Duplicate(field.NewPath("standardListTypeBetaKey").Index(1), MapItem{Key: "i", Value: 2}), + + // Case 10: Beta listType, Beta Key -> Beta Error + field.Duplicate(field.NewPath("betaListTypeBetaKey").Index(1), MapItem{Key: "j", Value: 2}).MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/zz_generated.validations.go new file mode 100644 index 00000000000..d385265c5c0 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listkeys/zz_generated.validations.go @@ -0,0 +1,240 @@ +//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 listkeys + +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 { + // type ListKeyStruct + scheme.AddValidationFunc((*ListKeyStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ListKeyStruct(ctx, op, nil /* fldPath */, obj.(*ListKeyStruct), safe.Cast[*ListKeyStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_ListKeyStruct validates an instance of ListKeyStruct according +// to declarative validation rules in the API schema. +func Validate_ListKeyStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ListKeyStruct) (errs field.ErrorList) { + // field ListKeyStruct.TypeMeta has no validation + + // field ListKeyStruct.AlphaListTypeStandardKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key }).MarkAlpha()...) + return + }(fldPath.Child("alphaListTypeStandardKey"), obj.AlphaListTypeStandardKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.AlphaListTypeStandardKey }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeAlphaKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + return + }(fldPath.Child("standardListTypeAlphaKey"), obj.StandardListTypeAlphaKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.StandardListTypeAlphaKey }), oldObj != nil)...) + + // field ListKeyStruct.AlphaListTypeAlphaKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key }).MarkAlpha()...) + return + }(fldPath.Child("alphaListTypeAlphaKey"), obj.AlphaListTypeAlphaKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.AlphaListTypeAlphaKey }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeMixedKeys1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key2 == b.Key2 && a.Key1 == b.Key1 })...) + return + }(fldPath.Child("standardListTypeMixedKeys1"), obj.StandardListTypeMixedKeys1, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.StandardListTypeMixedKeys1 }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeMixedKeys2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key1 == b.Key1 && a.Key2 == b.Key2 })...) + return + }(fldPath.Child("standardListTypeMixedKeys2"), obj.StandardListTypeMixedKeys2, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.StandardListTypeMixedKeys2 }), oldObj != nil)...) + + // field ListKeyStruct.AlphaListTypeMixedKeys1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key2 == b.Key2 && a.Key1 == b.Key1 }).MarkAlpha()...) + return + }(fldPath.Child("alphaListTypeMixedKeys1"), obj.AlphaListTypeMixedKeys1, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.AlphaListTypeMixedKeys1 }), oldObj != nil)...) + + // field ListKeyStruct.AlphaListTypeMixedKeys2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key1 == b.Key1 && a.Key2 == b.Key2 }).MarkAlpha()...) + return + }(fldPath.Child("alphaListTypeMixedKeys2"), obj.AlphaListTypeMixedKeys2, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.AlphaListTypeMixedKeys2 }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeMixedKeysBeta1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key2 == b.Key2 && a.Key1 == b.Key1 })...) + return + }(fldPath.Child("standardListTypeMixedKeysBeta1"), obj.StandardListTypeMixedKeysBeta1, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.StandardListTypeMixedKeysBeta1 }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeMixedKeysBeta2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key1 == b.Key1 && a.Key2 == b.Key2 })...) + return + }(fldPath.Child("standardListTypeMixedKeysBeta2"), obj.StandardListTypeMixedKeysBeta2, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.StandardListTypeMixedKeysBeta2 }), oldObj != nil)...) + + // field ListKeyStruct.BetaListTypeMixedKeys1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key2 == b.Key2 && a.Key1 == b.Key1 }).MarkBeta()...) + return + }(fldPath.Child("betaListTypeMixedKeys1"), obj.BetaListTypeMixedKeys1, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.BetaListTypeMixedKeys1 }), oldObj != nil)...) + + // field ListKeyStruct.BetaListTypeMixedKeys2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MultiKeyItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MultiKeyItem, b MultiKeyItem) bool { return a.Key1 == b.Key1 && a.Key2 == b.Key2 }).MarkBeta()...) + return + }(fldPath.Child("betaListTypeMixedKeys2"), obj.BetaListTypeMixedKeys2, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MultiKeyItem { return oldObj.BetaListTypeMixedKeys2 }), oldObj != nil)...) + + // field ListKeyStruct.BetaListTypeStandardKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key }).MarkBeta()...) + return + }(fldPath.Child("betaListTypeStandardKey"), obj.BetaListTypeStandardKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.BetaListTypeStandardKey }), oldObj != nil)...) + + // field ListKeyStruct.StandardListTypeBetaKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + return + }(fldPath.Child("standardListTypeBetaKey"), obj.StandardListTypeBetaKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.StandardListTypeBetaKey }), oldObj != nil)...) + + // field ListKeyStruct.BetaListTypeBetaKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key }).MarkBeta()...) + return + }(fldPath.Child("betaListTypeBetaKey"), obj.BetaListTypeBetaKey, safe.Field(oldObj, func(oldObj *ListKeyStruct) []MapItem { return oldObj.BetaListTypeBetaKey }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc.go new file mode 100644 index 00000000000..953b65de51a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc.go @@ -0,0 +1,79 @@ +/* +Copyright 2025 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 + +package listmapitem + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type MapItem struct { + Key string `json:"key"` + Value int `json:"value"` +} + +type InnerItem struct { + Value int `json:"value"` +} + +type ListMapItemStruct struct { + TypeMeta int + + // Case: Standard Item + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:minimum=10 + StandardItem []MapItem `json:"standardItem"` + + // Case: Alpha Item tag + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:alpha=+k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:minimum=10 + AlphaItemTag []MapItem `json:"alphaItemTag"` + + // Case: Alpha validation + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:alpha=+k8s:minimum=10 + AlphaValidation []MapItem `json:"alphaValidation"` + + // Case: Double Alpha + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:alpha=+k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:alpha=+k8s:minimum=10 + DoubleAlpha []MapItem `json:"doubleAlpha"` + + // Case: Beta Item tag + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:beta=+k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:minimum=10 + BetaItemTag []MapItem `json:"betaItemTag"` + + // Case: Beta validation + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:beta=+k8s:minimum=10 + BetaValidation []MapItem `json:"betaValidation"` + + // Case: Double Beta + // +k8s:listType=map + // +k8s:listMapKey=key + // +k8s:beta=+k8s:item(key: "foo")=+k8s:subfield(value)=+k8s:beta=+k8s:minimum=10 + DoubleBeta []MapItem `json:"doubleBeta"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc_test.go new file mode 100644 index 00000000000..282da8b0ff5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/doc_test.go @@ -0,0 +1,59 @@ +/* +Copyright 2025 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 listmapitem + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestListMapItem(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&ListMapItemStruct{ + StandardItem: []MapItem{{Key: "foo", Value: 5}}, + AlphaItemTag: []MapItem{{Key: "foo", Value: 5}}, + AlphaValidation: []MapItem{{Key: "foo", Value: 5}}, + DoubleAlpha: []MapItem{{Key: "foo", Value: 5}}, + + BetaItemTag: []MapItem{{Key: "foo", Value: 5}}, + BetaValidation: []MapItem{{Key: "foo", Value: 5}}, + DoubleBeta: []MapItem{{Key: "foo", Value: 5}}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + // Case 1: Standard Item -> Normal Error + field.Invalid(field.NewPath("standardItem").Index(0).Child("value"), 5, "").WithOrigin("minimum"), + + // Case 2: Alpha Item tag -> Alpha Error + field.Invalid(field.NewPath("alphaItemTag").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkAlpha(), + + // Case 3: Alpha validation -> Alpha Error + field.Invalid(field.NewPath("alphaValidation").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkAlpha(), + + // Case 4: Double Alpha -> Alpha Error + field.Invalid(field.NewPath("doubleAlpha").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkAlpha(), + + // Case 5: Beta Item tag -> Beta Error + field.Invalid(field.NewPath("betaItemTag").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkBeta(), + + // Case 6: Beta validation -> Beta Error + field.Invalid(field.NewPath("betaValidation").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkBeta(), + + // Case 7: Double Beta -> Beta Error + field.Invalid(field.NewPath("doubleBeta").Index(0).Child("value"), 5, "").WithOrigin("minimum").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/zz_generated.validations.go new file mode 100644 index 00000000000..5c7fa6a0015 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listmapitem/zz_generated.validations.go @@ -0,0 +1,198 @@ +//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 listmapitem + +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 { + // type ListMapItemStruct + scheme.AddValidationFunc((*ListMapItemStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ListMapItemStruct(ctx, op, nil /* fldPath */, obj.(*ListMapItemStruct), safe.Cast[*ListMapItemStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_ListMapItemStruct validates an instance of ListMapItemStruct according +// to declarative validation rules in the API schema. +func Validate_ListMapItemStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ListMapItemStruct) (errs field.ErrorList) { + // field ListMapItemStruct.TypeMeta has no validation + + // field ListMapItemStruct.StandardItem + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }) + })...) + }() + return + }(fldPath.Child("standardItem"), obj.StandardItem, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.StandardItem }), oldObj != nil)...) + + // field ListMapItemStruct.AlphaItemTag + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }) + }).MarkAlpha()...) + }() + return + }(fldPath.Child("alphaItemTag"), obj.AlphaItemTag, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.AlphaItemTag }), oldObj != nil)...) + + // field ListMapItemStruct.AlphaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha() + }) + })...) + }() + return + }(fldPath.Child("alphaValidation"), obj.AlphaValidation, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.AlphaValidation }), oldObj != nil)...) + + // field ListMapItemStruct.DoubleAlpha + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha() + }) + }).MarkAlpha()...) + }() + return + }(fldPath.Child("doubleAlpha"), obj.DoubleAlpha, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.DoubleAlpha }), oldObj != nil)...) + + // field ListMapItemStruct.BetaItemTag + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }) + }).MarkBeta()...) + }() + return + }(fldPath.Child("betaItemTag"), obj.BetaItemTag, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.BetaItemTag }), oldObj != nil)...) + + // field ListMapItemStruct.BetaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta() + }) + })...) + }() + return + }(fldPath.Child("betaValidation"), obj.BetaValidation, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.BetaValidation }), oldObj != nil)...) + + // field ListMapItemStruct.DoubleBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []MapItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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 MapItem, b MapItem) bool { return a.Key == b.Key })...) + func() { // cohort {"key": "foo"} + errs = append(errs, validate.SliceItem(ctx, op, fldPath, obj, oldObj, func(item *MapItem) bool { return item.Key == "foo" }, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *MapItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta() + }) + }).MarkBeta()...) + }() + return + }(fldPath.Child("doubleBeta"), obj.DoubleBeta, safe.Field(oldObj, func(oldObj *ListMapItemStruct) []MapItem { return oldObj.DoubleBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc.go new file mode 100644 index 00000000000..ce4640ce68a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc.go @@ -0,0 +1,71 @@ +/* +Copyright 2025 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 + +package listset + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type ListSetStruct struct { + TypeMeta int + + // Case: Alpha listType=set + // +k8s:alpha=+k8s:listType=set + Set []ComplexSetItem `json:"set"` + + // Case: Beta listType=set + // +k8s:beta=+k8s:listType=set + BetaSet []ComplexSetItem `json:"betaSet"` + + // Case: Chained subfield validation + // +k8s:listType=set + // +k8s:eachVal=+k8s:subfield(value)=+k8s:alpha=+k8s:minimum=10 + ChainedSubfieldSet []SimpleSetItem `json:"chainedSubfieldSet"` + + // Case: Alpha listType=set, Beta item validation + // +k8s:alpha=+k8s:listType=set + SetBetaItem []ComplexSetItemBeta `json:"setBetaItem"` + + // Case: Beta listType=set, Beta item validation + // +k8s:beta=+k8s:listType=set + BetaSetBetaItem []ComplexSetItemBeta `json:"betaSetBetaItem"` + + // Case: Chained subfield validation (Beta) + // +k8s:listType=set + // +k8s:eachVal=+k8s:subfield(value)=+k8s:beta=+k8s:minimum=10 + ChainedSubfieldSetBeta []SimpleSetItem `json:"chainedSubfieldSetBeta"` +} + +type ComplexSetItem struct { + // +k8s:alpha=+k8s:minimum=10 + Value int `json:"value"` + StringVal string `json:"stringVal"` +} + +type ComplexSetItemBeta struct { + // +k8s:beta=+k8s:minimum=10 + Value int `json:"value"` + StringVal string `json:"stringVal"` +} + +type SimpleSetItem struct { + Value int `json:"value"` + StringVal string `json:"stringVal"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc_test.go new file mode 100644 index 00000000000..24ffc0e1476 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/doc_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2025 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 listset + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestListSet(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&ListSetStruct{ + Set: []ComplexSetItem{ + {StringVal: "abc", Value: 10}, + {StringVal: "abc", Value: 10}, // Duplicate value (alpha) + }, + BetaSet: []ComplexSetItem{ + {StringVal: "abc", Value: 10}, + {StringVal: "abc", Value: 10}, // Duplicate value (beta) + }, + ChainedSubfieldSet: []SimpleSetItem{ + {StringVal: "def", Value: 9}, // Value 9 < 10 (alpha) + }, + SetBetaItem: []ComplexSetItemBeta{ + {StringVal: "ghi", Value: 10}, + {StringVal: "ghi", Value: 10}, // Duplicate value (alpha) + }, + BetaSetBetaItem: []ComplexSetItemBeta{ + {StringVal: "jkl", Value: 10}, + {StringVal: "jkl", Value: 10}, // Duplicate value (beta) + }, + ChainedSubfieldSetBeta: []SimpleSetItem{ + {StringVal: "mno", Value: 9}, // Value 9 < 10 (beta) + }, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + // Set: Duplicate value is SHADOWED + field.Duplicate(field.NewPath("set").Index(1), ComplexSetItem{StringVal: "abc", Value: 10}).MarkAlpha(), + + // BetaSet: Duplicate value is SHADOWED + field.Duplicate(field.NewPath("betaSet").Index(1), ComplexSetItem{StringVal: "abc", Value: 10}).MarkBeta(), + + // ChainedSubfieldSet: Value 9 < 10 is SHADOWED + field.Invalid(field.NewPath("chainedSubfieldSet").Index(0).Child("value"), 9, "").WithOrigin("minimum").MarkAlpha(), + + // SetBetaItem: Duplicate value is SHADOWED (Alpha list) + field.Duplicate(field.NewPath("setBetaItem").Index(1), ComplexSetItemBeta{StringVal: "ghi", Value: 10}).MarkAlpha(), + + // BetaSetBetaItem: Duplicate value is SHADOWED (Beta list) + field.Duplicate(field.NewPath("betaSetBetaItem").Index(1), ComplexSetItemBeta{StringVal: "jkl", Value: 10}).MarkBeta(), + + // ChainedSubfieldSetBeta: Value 9 < 10 is SHADOWED (Beta item) + field.Invalid(field.NewPath("chainedSubfieldSetBeta").Index(0).Child("value"), 9, "").WithOrigin("minimum").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/zz_generated.validations.go new file mode 100644 index 00000000000..3840e6a0c7a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/listset/zz_generated.validations.go @@ -0,0 +1,192 @@ +//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 listset + +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 { + // type ListSetStruct + scheme.AddValidationFunc((*ListSetStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ListSetStruct(ctx, op, nil /* fldPath */, obj.(*ListSetStruct), safe.Cast[*ListSetStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_ComplexSetItem validates an instance of ComplexSetItem according +// to declarative validation rules in the API schema. +func Validate_ComplexSetItem(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ComplexSetItem) (errs field.ErrorList) { + // field ComplexSetItem.Value + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha()...) + return + }(fldPath.Child("value"), &obj.Value, safe.Field(oldObj, func(oldObj *ComplexSetItem) *int { return &oldObj.Value }), oldObj != nil)...) + + // field ComplexSetItem.StringVal has no validation + return errs +} + +// Validate_ComplexSetItemBeta validates an instance of ComplexSetItemBeta according +// to declarative validation rules in the API schema. +func Validate_ComplexSetItemBeta(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ComplexSetItemBeta) (errs field.ErrorList) { + // field ComplexSetItemBeta.Value + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta()...) + return + }(fldPath.Child("value"), &obj.Value, safe.Field(oldObj, func(oldObj *ComplexSetItemBeta) *int { return &oldObj.Value }), oldObj != nil)...) + + // field ComplexSetItemBeta.StringVal has no validation + return errs +} + +// Validate_ListSetStruct validates an instance of ListSetStruct according +// to declarative validation rules in the API schema. +func Validate_ListSetStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ListSetStruct) (errs field.ErrorList) { + // field ListSetStruct.TypeMeta has no validation + + // field ListSetStruct.Set + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ComplexSetItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkAlpha()...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, Validate_ComplexSetItem)...) + return + }(fldPath.Child("set"), obj.Set, safe.Field(oldObj, func(oldObj *ListSetStruct) []ComplexSetItem { return oldObj.Set }), oldObj != nil)...) + + // field ListSetStruct.BetaSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ComplexSetItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkBeta()...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, Validate_ComplexSetItem)...) + return + }(fldPath.Child("betaSet"), obj.BetaSet, safe.Field(oldObj, func(oldObj *ListSetStruct) []ComplexSetItem { return oldObj.BetaSet }), oldObj != nil)...) + + // field ListSetStruct.ChainedSubfieldSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []SimpleSetItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, validate.DirectEqual, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SimpleSetItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *SimpleSetItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha() + }) + })...) + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) + return + }(fldPath.Child("chainedSubfieldSet"), obj.ChainedSubfieldSet, safe.Field(oldObj, func(oldObj *ListSetStruct) []SimpleSetItem { return oldObj.ChainedSubfieldSet }), oldObj != nil)...) + + // field ListSetStruct.SetBetaItem + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ComplexSetItemBeta, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkAlpha()...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, Validate_ComplexSetItemBeta)...) + return + }(fldPath.Child("setBetaItem"), obj.SetBetaItem, safe.Field(oldObj, func(oldObj *ListSetStruct) []ComplexSetItemBeta { return oldObj.SetBetaItem }), oldObj != nil)...) + + // field ListSetStruct.BetaSetBetaItem + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []ComplexSetItemBeta, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkBeta()...) + // iterate the list and call the type's validation function + errs = append(errs, validate.EachSliceVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, nil, Validate_ComplexSetItemBeta)...) + return + }(fldPath.Child("betaSetBetaItem"), obj.BetaSetBetaItem, safe.Field(oldObj, func(oldObj *ListSetStruct) []ComplexSetItemBeta { return oldObj.BetaSetBetaItem }), oldObj != nil)...) + + // field ListSetStruct.ChainedSubfieldSetBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []SimpleSetItem, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && 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, validate.DirectEqual, nil, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SimpleSetItem) field.ErrorList { + return validate.Subfield(ctx, op, fldPath, obj, oldObj, "value", func(o *SimpleSetItem) *int { return &o.Value }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta() + }) + })...) + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual)...) + return + }(fldPath.Child("chainedSubfieldSetBeta"), obj.ChainedSubfieldSetBeta, safe.Field(oldObj, func(oldObj *ListSetStruct) []SimpleSetItem { return oldObj.ChainedSubfieldSetBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc.go new file mode 100644 index 00000000000..85c919d8a7e --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc.go @@ -0,0 +1,60 @@ +/* +Copyright 2025 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 + +package mapvalidation + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type MapValidationStruct struct { + TypeMeta int + + // Case: Standard eachVal + // +k8s:eachVal=+k8s:maxLength=2 + StandardEachVal map[string]string `json:"standardEachVal"` + + // Case: Alpha eachVal + // +k8s:alpha=+k8s:eachVal=+k8s:maxLength=2 + AlphaEachVal map[string]string `json:"AlphaEachVal"` + + // Case: Beta eachVal + // +k8s:beta=+k8s:eachVal=+k8s:maxLength=2 + BetaEachVal map[string]string `json:"BetaEachVal"` + + // Case: Standard eachKey + // +k8s:eachKey=+k8s:maxLength=2 + StandardEachKey map[string]string `json:"standardEachKey"` + + // Case: Alpha eachKey + // +k8s:alpha=+k8s:eachKey=+k8s:maxLength=2 + AlphaEachKey map[string]string `json:"AlphaEachKey"` + + // Case: Beta eachKey + // +k8s:beta=+k8s:eachKey=+k8s:maxLength=2 + BetaEachKey map[string]string `json:"BetaEachKey"` + + // Case: Alpha Validation + // +k8s:eachVal=+k8s:alpha=+k8s:maxLength=2 + AlphaValidation map[string]string `json:"AlphaValidation"` + + // Case: Beta Validation + // +k8s:eachVal=+k8s:beta=+k8s:maxLength=2 + BetaValidation map[string]string `json:"BetaValidation"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc_test.go new file mode 100644 index 00000000000..b8239c06e5e --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/doc_test.go @@ -0,0 +1,62 @@ +/* +Copyright 2025 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 mapvalidation + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestMapValidation(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&MapValidationStruct{ + StandardEachVal: map[string]string{"a": "foo"}, + AlphaEachVal: map[string]string{"a": "foo"}, + BetaEachVal: map[string]string{"a": "foo"}, + StandardEachKey: map[string]string{"foo": "a"}, + AlphaEachKey: map[string]string{"foo": "a"}, + BetaEachKey: map[string]string{"foo": "a"}, + AlphaValidation: map[string]string{"a": "foo"}, + BetaValidation: map[string]string{"a": "foo"}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + // Case: Standard eachVal -> Normal Error + field.TooLong(field.NewPath("standardEachVal").Key("a"), "foo", 2).WithOrigin("maxLength"), + + // Case: Alpha eachVal -> Alpha Error + field.TooLong(field.NewPath("AlphaEachVal").Key("a"), "foo", 2).WithOrigin("maxLength").MarkAlpha(), + + // Case: Beta eachVal -> Beta Error + field.TooLong(field.NewPath("BetaEachVal").Key("a"), "foo", 2).WithOrigin("maxLength").MarkBeta(), + + // Case: Standard eachKey -> Normal Error + field.TooLong(field.NewPath("standardEachKey"), "foo", 2).WithOrigin("maxLength"), + + // Case: Alpha eachKey -> Alpha Error + field.TooLong(field.NewPath("AlphaEachKey"), "foo", 2).WithOrigin("maxLength").MarkAlpha(), + + // Case: Beta eachKey -> Beta Error + field.TooLong(field.NewPath("BetaEachKey"), "foo", 2).WithOrigin("maxLength").MarkBeta(), + + // Case: Alpha Validation -> Alpha Error + field.TooLong(field.NewPath("AlphaValidation").Key("a"), "foo", 2).WithOrigin("maxLength").MarkAlpha(), + + // Case: Beta Validation -> Beta Error + field.TooLong(field.NewPath("BetaValidation").Key("a"), "foo", 2).WithOrigin("maxLength").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/zz_generated.validations.go new file mode 100644 index 00000000000..8b774c2311a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/mapvalidation/zz_generated.validations.go @@ -0,0 +1,170 @@ +//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 mapvalidation + +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 { + // type MapValidationStruct + scheme.AddValidationFunc((*MapValidationStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_MapValidationStruct(ctx, op, nil /* fldPath */, obj.(*MapValidationStruct), safe.Cast[*MapValidationStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_MapValidationStruct validates an instance of MapValidationStruct according +// to declarative validation rules in the API schema. +func Validate_MapValidationStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MapValidationStruct) (errs field.ErrorList) { + // field MapValidationStruct.TypeMeta has no validation + + // field MapValidationStruct.StandardEachVal + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + })...) + return + }(fldPath.Child("standardEachVal"), obj.StandardEachVal, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.StandardEachVal }), oldObj != nil)...) + + // field MapValidationStruct.AlphaEachVal + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + }).MarkAlpha()...) + return + }(fldPath.Child("AlphaEachVal"), obj.AlphaEachVal, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.AlphaEachVal }), oldObj != nil)...) + + // field MapValidationStruct.BetaEachVal + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + }).MarkBeta()...) + return + }(fldPath.Child("BetaEachVal"), obj.BetaEachVal, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.BetaEachVal }), oldObj != nil)...) + + // field MapValidationStruct.StandardEachKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + })...) + return + }(fldPath.Child("standardEachKey"), obj.StandardEachKey, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.StandardEachKey }), oldObj != nil)...) + + // field MapValidationStruct.AlphaEachKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + }).MarkAlpha()...) + return + }(fldPath.Child("AlphaEachKey"), obj.AlphaEachKey, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.AlphaEachKey }), oldObj != nil)...) + + // field MapValidationStruct.BetaEachKey + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapKey(ctx, op, fldPath, obj, oldObj, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2) + }).MarkBeta()...) + return + }(fldPath.Child("BetaEachKey"), obj.BetaEachKey, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.BetaEachKey }), oldObj != nil)...) + + // field MapValidationStruct.AlphaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2).MarkAlpha() + })...) + return + }(fldPath.Child("AlphaValidation"), obj.AlphaValidation, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.AlphaValidation }), oldObj != nil)...) + + // field MapValidationStruct.BetaValidation + errs = append(errs, + func(fldPath *field.Path, obj, oldObj map[string]string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + errs = append(errs, validate.EachMapVal(ctx, op, fldPath, obj, oldObj, validate.DirectEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + return validate.MaxLength(ctx, op, fldPath, obj, oldObj, 2).MarkBeta() + })...) + return + }(fldPath.Child("BetaValidation"), obj.BetaValidation, safe.Field(oldObj, func(oldObj *MapValidationStruct) map[string]string { return oldObj.BetaValidation }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc.go new file mode 100644 index 00000000000..5e6188d44d5 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc.go @@ -0,0 +1,34 @@ +/* +Copyright 2025 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 + +package optionalrequired + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:alpha=+k8s:required + RequiredField *string `json:"requiredField"` + + // +k8s:beta=+k8s:required + RequiredFieldBeta *string `json:"requiredFieldBeta"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc_test.go new file mode 100644 index 00000000000..92cb8ee44c1 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/doc_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2025 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 optionalrequired + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + RequiredField: ptr.To("val"), + RequiredFieldBeta: ptr.To("val"), + }).ExpectValid() + + // Test failures marked as alpha + st.Value(&Struct{ + RequiredField: nil, + RequiredFieldBeta: ptr.To("val"), + }).OldValue(&Struct{ + RequiredField: ptr.To("old"), + RequiredFieldBeta: ptr.To("old"), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("requiredField"), "").MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + RequiredField: ptr.To("val"), + RequiredFieldBeta: ptr.To("val"), + }).ExpectValid() + + // Test failures marked as beta + st.Value(&Struct{ + RequiredField: ptr.To("val"), + RequiredFieldBeta: nil, + }).OldValue(&Struct{ + RequiredField: ptr.To("old"), + RequiredFieldBeta: ptr.To("old"), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("requiredFieldBeta"), "").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/zz_generated.validations.go new file mode 100644 index 00000000000..784e68b563b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/optionalrequired/zz_generated.validations.go @@ -0,0 +1,95 @@ +//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 optionalrequired + +import ( + context "context" + fmt "fmt" + + 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 { + // type Struct + 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 +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.RequiredField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("requiredField"), obj.RequiredField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.RequiredField }), oldObj != nil)...) + + // field Struct.RequiredFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("requiredFieldBeta"), obj.RequiredFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.RequiredFieldBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc.go new file mode 100644 index 00000000000..9c70187874f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc.go @@ -0,0 +1,90 @@ +/* +Copyright 2025 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 + +package simple + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:alpha=+k8s:minimum=10 + IntField int `json:"intField"` + + // +k8s:beta=+k8s:minimum=10 + IntFieldBeta int `json:"intFieldBeta"` + + // +k8s:alpha=+k8s:maxLength=5 + StringField string `json:"stringField"` + + // +k8s:beta=+k8s:maxLength=5 + StringFieldBeta string `json:"stringFieldBeta"` + + // +k8s:alpha=+k8s:maxItems=2 + SliceField []string `json:"sliceField"` + + // +k8s:beta=+k8s:maxItems=2 + SliceFieldBeta []string `json:"sliceFieldBeta"` + + // +k8s:alpha=+k8s:format=k8s-uuid + UUIDField string `json:"uuidField"` + + // +k8s:beta=+k8s:format=k8s-uuid + UUIDFieldBeta string `json:"uuidFieldBeta"` + + // +k8s:alpha=+k8s:immutable + ImmutableField string `json:"immutableField"` + + // +k8s:beta=+k8s:immutable + ImmutableFieldBeta string `json:"immutableFieldBeta"` +} + +type SpecialValidationStruct struct { + TypeMeta int + + // +k8s:alpha=+k8s:neq=5 + NEQField int `json:"neqField"` + + // +k8s:beta=+k8s:neq=5 + NEQFieldBeta int `json:"neqFieldBeta"` + + // +k8s:alpha=+k8s:forbidden + ForbiddenField *string `json:"forbiddenField"` + + // +k8s:beta=+k8s:forbidden + ForbiddenFieldBeta *string `json:"forbiddenFieldBeta"` + + // +k8s:alpha=+k8s:update=NoModify + UpdateField string `json:"updateField"` + + // +k8s:beta=+k8s:update=NoModify + UpdateFieldBeta string `json:"updateFieldBeta"` +} + +type StructWithValidateFalse struct { + TypeMeta int + + // +k8s:alpha=+k8s:validateFalse="always fails" + ValidateFalse *string `json:"validateFalse"` + + // +k8s:beta=+k8s:validateFalse="always fails" + ValidateFalseBeta *string `json:"validateFalseBeta"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc_test.go new file mode 100644 index 00000000000..d768f825c40 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/doc_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2025 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 TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + IntField: 10, + StringField: "abc", + SliceField: []string{"a", "b"}, + UUIDField: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableField: "foo", + IntFieldBeta: 10, + StringFieldBeta: "abc", + SliceFieldBeta: []string{"a", "b"}, + UUIDFieldBeta: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableFieldBeta: "foo", + }).ExpectValid() + + // Test failures marked as alpha + st.Value(&Struct{ + IntField: 5, + StringField: "too-long", + SliceField: []string{"a", "b", "c"}, + UUIDField: "not-a-uuid", + ImmutableField: "bar", + IntFieldBeta: 10, + StringFieldBeta: "abc", + SliceFieldBeta: []string{"a", "b"}, + UUIDFieldBeta: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableFieldBeta: "foo", + }).OldValue(&Struct{ + ImmutableField: "foo", + ImmutableFieldBeta: "foo", + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("intField"), 5, "").WithOrigin("minimum").MarkAlpha(), + field.TooLong(field.NewPath("stringField"), "too-long", 5).WithOrigin("maxLength").MarkAlpha(), + field.TooMany(field.NewPath("sliceField"), 3, 2).WithOrigin("maxItems").MarkAlpha(), + field.Invalid(field.NewPath("uuidField"), "not-a-uuid", "").WithOrigin("format=k8s-uuid").MarkAlpha(), + field.Invalid(field.NewPath("immutableField"), "bar", "").WithOrigin("immutable").MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + IntField: 10, + StringField: "abc", + SliceField: []string{"a", "b"}, + UUIDField: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableField: "foo", + IntFieldBeta: 10, + StringFieldBeta: "abc", + SliceFieldBeta: []string{"a", "b"}, + UUIDFieldBeta: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableFieldBeta: "foo", + }).ExpectValid() + + // Test failures marked as beta + st.Value(&Struct{ + IntField: 10, + StringField: "abc", + SliceField: []string{"a", "b"}, + UUIDField: "a0a2a2d2-0b87-4964-a123-78d00a8787a6", + ImmutableField: "foo", + IntFieldBeta: 5, + StringFieldBeta: "too-long", + SliceFieldBeta: []string{"a", "b", "c"}, + UUIDFieldBeta: "not-a-uuid", + ImmutableFieldBeta: "bar", + }).OldValue(&Struct{ + ImmutableField: "foo", + ImmutableFieldBeta: "foo", + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("intFieldBeta"), 5, "").WithOrigin("minimum").MarkBeta(), + field.TooLong(field.NewPath("stringFieldBeta"), "too-long", 5).WithOrigin("maxLength").MarkBeta(), + field.TooMany(field.NewPath("sliceFieldBeta"), 3, 2).WithOrigin("maxItems").MarkBeta(), + field.Invalid(field.NewPath("uuidFieldBeta"), "not-a-uuid", "").WithOrigin("format=k8s-uuid").MarkBeta(), + field.Invalid(field.NewPath("immutableFieldBeta"), "bar", "").WithOrigin("immutable").MarkBeta(), + }) +} + +func TestSpecialValidationStruct(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&SpecialValidationStruct{ + NEQField: 10, + NEQFieldBeta: 10, + }).ExpectValid() + + st.Value(&SpecialValidationStruct{ + NEQField: 5, + NEQFieldBeta: 5, + ForbiddenField: ptr.To("val"), + ForbiddenFieldBeta: ptr.To("val"), + UpdateField: "new", + UpdateFieldBeta: "new", + }).OldValue(&SpecialValidationStruct{ + UpdateField: "old", + UpdateFieldBeta: "old", + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("neqField"), 5, "must not be 5").WithOrigin("neq").MarkAlpha(), + field.Invalid(field.NewPath("neqFieldBeta"), 5, "must not be 5").WithOrigin("neq").MarkBeta(), + field.Forbidden(field.NewPath("forbiddenField"), "").MarkAlpha(), + field.Forbidden(field.NewPath("forbiddenFieldBeta"), "").MarkBeta(), + field.Invalid(field.NewPath("updateField"), "new", "field cannot be modified once set").WithOrigin("update").MarkAlpha(), + field.Invalid(field.NewPath("updateFieldBeta"), "new", "field cannot be modified once set").WithOrigin("update").MarkBeta(), + }) + + st.Value(&StructWithValidateFalse{ + ValidateFalse: ptr.To("val"), + ValidateFalseBeta: ptr.To("val"), + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("validateFalse"), "val", "always fails").MarkAlpha(), + field.Invalid(field.NewPath("validateFalseBeta"), "val", "always fails").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/zz_generated.validations.go new file mode 100644 index 00000000000..f3a6a062f3c --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/simple/zz_generated.validations.go @@ -0,0 +1,368 @@ +//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 { + // type SpecialValidationStruct + scheme.AddValidationFunc((*SpecialValidationStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_SpecialValidationStruct(ctx, op, nil /* fldPath */, obj.(*SpecialValidationStruct), safe.Cast[*SpecialValidationStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type Struct + 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()))} + }) + // type StructWithValidateFalse + scheme.AddValidationFunc((*StructWithValidateFalse)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_StructWithValidateFalse(ctx, op, nil /* fldPath */, obj.(*StructWithValidateFalse), safe.Cast[*StructWithValidateFalse](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_SpecialValidationStruct validates an instance of SpecialValidationStruct according +// to declarative validation rules in the API schema. +func Validate_SpecialValidationStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SpecialValidationStruct) (errs field.ErrorList) { + // field SpecialValidationStruct.TypeMeta has no validation + + // field SpecialValidationStruct.NEQField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.NEQ(ctx, op, fldPath, obj, oldObj, 5).MarkAlpha()...) + return + }(fldPath.Child("neqField"), &obj.NEQField, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *int { return &oldObj.NEQField }), oldObj != nil)...) + + // field SpecialValidationStruct.NEQFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.NEQ(ctx, op, fldPath, obj, oldObj, 5).MarkBeta()...) + return + }(fldPath.Child("neqFieldBeta"), &obj.NEQFieldBeta, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *int { return &oldObj.NEQFieldBeta }), oldObj != nil)...) + + // field SpecialValidationStruct.ForbiddenField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("forbiddenField"), obj.ForbiddenField, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *string { return oldObj.ForbiddenField }), oldObj != nil)...) + + // field SpecialValidationStruct.ForbiddenFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("forbiddenFieldBeta"), obj.ForbiddenFieldBeta, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *string { return oldObj.ForbiddenFieldBeta }), oldObj != nil)...) + + // field SpecialValidationStruct.UpdateField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("updateField"), &obj.UpdateField, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *string { return &oldObj.UpdateField }), oldObj != nil)...) + + // field SpecialValidationStruct.UpdateFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("updateFieldBeta"), &obj.UpdateFieldBeta, safe.Field(oldObj, func(oldObj *SpecialValidationStruct) *string { return &oldObj.UpdateFieldBeta }), oldObj != nil)...) + + return errs +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.IntField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha()...) + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }), oldObj != nil)...) + + // field Struct.IntFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta()...) + return + }(fldPath.Child("intFieldBeta"), &obj.IntFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntFieldBeta }), oldObj != nil)...) + + // field Struct.StringField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 5).MarkAlpha()...) + return + }(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }), oldObj != nil)...) + + // field Struct.StringFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 5).MarkBeta()...) + return + }(fldPath.Child("stringFieldBeta"), &obj.StringFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringFieldBeta }), oldObj != nil)...) + + // field Struct.SliceField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("sliceField"), obj.SliceField, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.SliceField }), oldObj != nil)...) + + // field Struct.SliceFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 2).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("sliceFieldBeta"), obj.SliceFieldBeta, safe.Field(oldObj, func(oldObj *Struct) []string { return oldObj.SliceFieldBeta }), oldObj != nil)...) + + // field Struct.UUIDField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.UUID(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return + }(fldPath.Child("uuidField"), &obj.UUIDField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.UUIDField }), oldObj != nil)...) + + // field Struct.UUIDFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.UUID(ctx, op, fldPath, obj, oldObj).MarkBeta()...) + return + }(fldPath.Child("uuidFieldBeta"), &obj.UUIDFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.UUIDFieldBeta }), oldObj != nil)...) + + // field Struct.ImmutableField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("immutableField"), &obj.ImmutableField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.ImmutableField }), oldObj != nil)...) + + // field Struct.ImmutableFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.Immutable(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("immutableFieldBeta"), &obj.ImmutableFieldBeta, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.ImmutableFieldBeta }), oldObj != nil)...) + + return errs +} + +// Validate_StructWithValidateFalse validates an instance of StructWithValidateFalse according +// to declarative validation rules in the API schema. +func Validate_StructWithValidateFalse(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *StructWithValidateFalse) (errs field.ErrorList) { + // field StructWithValidateFalse.TypeMeta has no validation + + // field StructWithValidateFalse.ValidateFalse + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "always fails").MarkAlpha()...) + return + }(fldPath.Child("validateFalse"), obj.ValidateFalse, safe.Field(oldObj, func(oldObj *StructWithValidateFalse) *string { return oldObj.ValidateFalse }), oldObj != nil)...) + + // field StructWithValidateFalse.ValidateFalseBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "always fails").MarkBeta()...) + return + }(fldPath.Child("validateFalseBeta"), obj.ValidateFalseBeta, safe.Field(oldObj, func(oldObj *StructWithValidateFalse) *string { return oldObj.ValidateFalseBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc.go new file mode 100644 index 00000000000..28e94b7174d --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc.go @@ -0,0 +1,60 @@ +/* +Copyright 2025 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 + +package structs + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type MixedStruct struct { + TypeMeta int + + // +k8s:minimum=5 + // +k8s:alpha=+k8s:minimum=10 + IntField int `json:"intField"` + + // +k8s:minimum=5 + // +k8s:beta=+k8s:minimum=10 + IntFieldBeta int `json:"intFieldBeta"` + + // +k8s:maxItems=5 + // +k8s:alpha=+k8s:maxItems=3 + ListField []string `json:"listField"` + + // +k8s:maxItems=5 + // +k8s:beta=+k8s:maxItems=3 + ListFieldBeta []string `json:"listFieldBeta"` +} + +type ConditionalStruct struct { + TypeMeta int + + // +k8s:alpha=+k8s:ifEnabled(MyFeature)=+k8s:minimum=10 + ConditionalField int `json:"conditionalField"` + + // +k8s:beta=+k8s:ifEnabled(MyFeature)=+k8s:minimum=10 + ConditionalFieldBeta int `json:"conditionalFieldBeta"` + + // +k8s:alpha=+k8s:alpha=+k8s:minimum=20 + RecursiveAlpha int `json:"recursiveAlpha"` + + // +k8s:beta=+k8s:beta=+k8s:minimum=20 + RecursiveBeta int `json:"recursiveBeta"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc_test.go new file mode 100644 index 00000000000..0707892f67b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/doc_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2025 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 structs + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestMixed(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid case (meets both normal and alpha requirements) + st.Value(&MixedStruct{ + IntField: 15, + IntFieldBeta: 15, + ListField: []string{"a", "b", "c"}, + ListFieldBeta: []string{"a", "b", "c"}, + }).ExpectValid() + + // Fails alpha validation but passes normal validation + // IntField: 5 <= 8 < 10 (alpha fails) + // ListField: 3 < 4 <= 5 (alpha fails) + st.Value(&MixedStruct{ + IntField: 8, + IntFieldBeta: 8, + ListField: []string{"a", "b", "c", "d"}, + ListFieldBeta: []string{"a", "b", "c", "d"}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("intField"), 8, "").WithOrigin("minimum").MarkAlpha(), + field.Invalid(field.NewPath("intFieldBeta"), 8, "").WithOrigin("minimum").MarkBeta(), + field.TooMany(field.NewPath("listField"), 4, 3).WithOrigin("maxItems").MarkAlpha(), + field.TooMany(field.NewPath("listFieldBeta"), 4, 3).WithOrigin("maxItems").MarkBeta(), + }) + + // Fails both normal and alpha validation + // IntField: 4 < 5 (normal fails) AND 4 < 10 (alpha fails) + // ListField: 6 > 5 (normal fails) AND 6 > 3 (alpha fails) + st.Value(&MixedStruct{ + IntField: 4, + IntFieldBeta: 4, + ListField: []string{"a", "b", "c", "d", "e", "f"}, + ListFieldBeta: []string{"a", "b", "c", "d", "e", "f"}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("intField"), 4, "").WithOrigin("minimum"), + field.Invalid(field.NewPath("intField"), 4, "").WithOrigin("minimum").MarkAlpha(), + field.Invalid(field.NewPath("intFieldBeta"), 4, "").WithOrigin("minimum"), + field.Invalid(field.NewPath("intFieldBeta"), 4, "").WithOrigin("minimum").MarkBeta(), + field.TooMany(field.NewPath("listField"), 6, 5).WithOrigin("maxItems"), + field.TooMany(field.NewPath("listField"), 6, 3).WithOrigin("maxItems").MarkAlpha(), + field.TooMany(field.NewPath("listFieldBeta"), 6, 5).WithOrigin("maxItems"), + field.TooMany(field.NewPath("listFieldBeta"), 6, 3).WithOrigin("maxItems").MarkBeta(), + }) +} + +func TestConditionalStruct(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&ConditionalStruct{ + ConditionalField: 15, + ConditionalFieldBeta: 15, + RecursiveAlpha: 25, + RecursiveBeta: 25, + }).Opts([]string{"MyFeature"}).ExpectValid() + + st.Value(&ConditionalStruct{ + ConditionalField: 5, + ConditionalFieldBeta: 5, + RecursiveAlpha: 10, + RecursiveBeta: 10, + }).Opts([]string{"MyFeature"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("conditionalField"), 5, "").WithOrigin("minimum").MarkAlpha(), + field.Invalid(field.NewPath("conditionalFieldBeta"), 5, "").WithOrigin("minimum").MarkBeta(), + field.Invalid(field.NewPath("recursiveAlpha"), 10, "").WithOrigin("minimum").MarkAlpha(), + field.Invalid(field.NewPath("recursiveBeta"), 10, "").WithOrigin("minimum").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/zz_generated.validations.go new file mode 100644 index 00000000000..123ecb700f0 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/structs/zz_generated.validations.go @@ -0,0 +1,198 @@ +//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 structs + +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 { + // type ConditionalStruct + scheme.AddValidationFunc((*ConditionalStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_ConditionalStruct(ctx, op, nil /* fldPath */, obj.(*ConditionalStruct), safe.Cast[*ConditionalStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type MixedStruct + scheme.AddValidationFunc((*MixedStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_MixedStruct(ctx, op, nil /* fldPath */, obj.(*MixedStruct), safe.Cast[*MixedStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_ConditionalStruct validates an instance of ConditionalStruct according +// to declarative validation rules in the API schema. +func Validate_ConditionalStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ConditionalStruct) (errs field.ErrorList) { + // field ConditionalStruct.TypeMeta has no validation + + // field ConditionalStruct.ConditionalField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "MyFeature", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }).MarkAlpha()...) + return + }(fldPath.Child("conditionalField"), &obj.ConditionalField, safe.Field(oldObj, func(oldObj *ConditionalStruct) *int { return &oldObj.ConditionalField }), oldObj != nil)...) + + // field ConditionalStruct.ConditionalFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "MyFeature", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 10) + }).MarkBeta()...) + return + }(fldPath.Child("conditionalFieldBeta"), &obj.ConditionalFieldBeta, safe.Field(oldObj, func(oldObj *ConditionalStruct) *int { return &oldObj.ConditionalFieldBeta }), oldObj != nil)...) + + // field ConditionalStruct.RecursiveAlpha + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 20).MarkAlpha()...) + return + }(fldPath.Child("recursiveAlpha"), &obj.RecursiveAlpha, safe.Field(oldObj, func(oldObj *ConditionalStruct) *int { return &oldObj.RecursiveAlpha }), oldObj != nil)...) + + // field ConditionalStruct.RecursiveBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 20).MarkBeta()...) + return + }(fldPath.Child("recursiveBeta"), &obj.RecursiveBeta, safe.Field(oldObj, func(oldObj *ConditionalStruct) *int { return &oldObj.RecursiveBeta }), oldObj != nil)...) + + return errs +} + +// Validate_MixedStruct validates an instance of MixedStruct according +// to declarative validation rules in the API schema. +func Validate_MixedStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedStruct) (errs field.ErrorList) { + // field MixedStruct.TypeMeta has no validation + + // field MixedStruct.IntField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 5)...) + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkAlpha()...) + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *MixedStruct) *int { return &oldObj.IntField }), oldObj != nil)...) + + // field MixedStruct.IntFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 5)...) + errs = append(errs, validate.Minimum(ctx, op, fldPath, obj, oldObj, 10).MarkBeta()...) + return + }(fldPath.Child("intFieldBeta"), &obj.IntFieldBeta, safe.Field(oldObj, func(oldObj *MixedStruct) *int { return &oldObj.IntFieldBeta }), oldObj != nil)...) + + // field MixedStruct.ListField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 5); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 3).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("listField"), obj.ListField, safe.Field(oldObj, func(oldObj *MixedStruct) []string { return oldObj.ListField }), oldObj != nil)...) + + // field MixedStruct.ListFieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 5); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if e := validate.MaxItems(ctx, op, fldPath, obj, oldObj, 3).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("listFieldBeta"), obj.ListFieldBeta, safe.Field(oldObj, func(oldObj *MixedStruct) []string { return oldObj.ListFieldBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc.go new file mode 100644 index 00000000000..a44f9ef958a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc.go @@ -0,0 +1,38 @@ +/* +Copyright 2025 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 + +package subfields + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:alpha=+k8s:subfield(inner)=+k8s:minimum=5 + Subfield SubStruct `json:"subfield"` + + // +k8s:beta=+k8s:subfield(inner)=+k8s:minimum=5 + SubfieldBeta SubStruct `json:"subfieldBeta"` +} + +type SubStruct struct { + Inner int `json:"inner"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc_test.go new file mode 100644 index 00000000000..b814855d25f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/doc_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 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 subfields + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + Subfield: SubStruct{Inner: 5}, + SubfieldBeta: SubStruct{Inner: 5}, + }).ExpectValid() + + // Test failures marked as alpha + st.Value(&Struct{ + Subfield: SubStruct{Inner: 1}, + SubfieldBeta: SubStruct{Inner: 5}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("subfield", "inner"), 1, "").WithOrigin("minimum").MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + Subfield: SubStruct{Inner: 5}, + SubfieldBeta: SubStruct{Inner: 5}, + }).ExpectValid() + + // Test failures marked as beta + st.Value(&Struct{ + Subfield: SubStruct{Inner: 5}, + SubfieldBeta: SubStruct{Inner: 1}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("subfieldBeta", "inner"), 1, "").WithOrigin("minimum").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/zz_generated.validations.go new file mode 100644 index 00000000000..e3acd723620 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/subfields/zz_generated.validations.go @@ -0,0 +1,89 @@ +//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 subfields + +import ( + context "context" + fmt "fmt" + + 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 { + // type Struct + 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 +} + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.Subfield + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *SubStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + func() { // cohort inner + errs = append(errs, validate.Subfield(ctx, op, fldPath, obj, oldObj, "inner", func(o *SubStruct) *int { return &o.Inner }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 5) + }).MarkAlpha()...) + }() + return + }(fldPath.Child("subfield"), &obj.Subfield, safe.Field(oldObj, func(oldObj *Struct) *SubStruct { return &oldObj.Subfield }), oldObj != nil)...) + + // field Struct.SubfieldBeta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *SubStruct, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + func() { // cohort inner + errs = append(errs, validate.Subfield(ctx, op, fldPath, obj, oldObj, "inner", func(o *SubStruct) *int { return &o.Inner }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *int) field.ErrorList { + return validate.Minimum(ctx, op, fldPath, obj, oldObj, 5) + }).MarkBeta()...) + }() + return + }(fldPath.Child("subfieldBeta"), &obj.SubfieldBeta, safe.Field(oldObj, func(oldObj *Struct) *SubStruct { return &oldObj.SubfieldBeta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc.go new file mode 100644 index 00000000000..c9347597de0 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc.go @@ -0,0 +1,104 @@ +/* +Copyright 2025 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 + +package unions + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:alpha=+k8s:unionDiscriminator + D D `json:"d"` + + // +k8s:alpha=+k8s:unionMember + // +k8s:optional + M1 *M1 `json:"m1"` + + // +k8s:alpha(since:"1.35")=+k8s:unionMember + // +k8s:optional + M2 *M2 `json:"m2"` +} + +type UnionStructBeta struct { + TypeMeta int + + // +k8s:beta=+k8s:unionDiscriminator + DBeta BetaD `json:"dBeta"` + + // +k8s:beta=+k8s:unionMember + // +k8s:optional + M1Beta *BetaM1 `json:"m1Beta"` + + // +k8s:beta(since:"1.35")=+k8s:unionMember + // +k8s:optional + M2Beta *BetaM2 `json:"m2Beta"` +} + +type MyStruct struct { + TypeMeta int + + // +k8s:alpha=+k8s:zeroOrOneOfMember + // +k8s:optional + Z1 *Z1 `json:"z1"` + + // +k8s:alpha=+k8s:zeroOrOneOfMember + // +k8s:optional + Z2 *Z2 `json:"z2"` +} + +type MyStructBeta struct { + TypeMeta int + + // +k8s:beta=+k8s:zeroOrOneOfMember + // +k8s:optional + Z1Beta *BetaZ1 `json:"z1Beta"` + + // +k8s:beta=+k8s:zeroOrOneOfMember + // +k8s:optional + Z2Beta *BetaZ2 `json:"z2Beta"` +} + +type D string + +const ( + DM1 D = "M1" + DM2 D = "M2" +) + +type M1 struct{} +type M2 struct{} + +type BetaD string + +const ( + BetaDM1 BetaD = "M1Beta" + BetaDM2 BetaD = "M2Beta" +) + +type BetaM1 struct{} +type BetaM2 struct{} + +type Z1 struct{} +type Z2 struct{} + +type BetaZ1 struct{} +type BetaZ2 struct{} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc_test.go new file mode 100644 index 00000000000..75f37e7536b --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/doc_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2025 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 unions + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + D: DM1, + M1: &M1{}, + }).ExpectValid() + + st.Value(&Struct{ + D: DM1, + M1: nil, // required by discriminator + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("m1"), nil, "").WithOrigin("union").MarkAlpha(), + }) + + st.Value(&MyStruct{ + Z1: &Z1{}, + Z2: &Z2{}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(nil, &MyStruct{ + Z1: &Z1{}, Z2: &Z2{}, + }, "only one of z1, z2 may be specified").WithOrigin("zeroOrOneOf").MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&UnionStructBeta{ + DBeta: BetaDM1, + M1Beta: &BetaM1{}, + }).ExpectValid() + + st.Value(&UnionStructBeta{ + DBeta: BetaDM1, + M1Beta: nil, // required by discriminator + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(field.NewPath("m1Beta"), nil, "").WithOrigin("union").MarkBeta(), + }) + + st.Value(&MyStructBeta{ + Z1Beta: &BetaZ1{}, + Z2Beta: &BetaZ2{}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Invalid(nil, &MyStructBeta{ + Z1Beta: &BetaZ1{}, Z2Beta: &BetaZ2{}, + }, "only one of z1Beta, z2Beta may be specified").WithOrigin("zeroOrOneOf").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/zz_generated.validations.go new file mode 100644 index 00000000000..298c3805b23 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/unions/zz_generated.validations.go @@ -0,0 +1,317 @@ +//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 unions + +import ( + context "context" + fmt "fmt" + + 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 { + // type MyStruct + scheme.AddValidationFunc((*MyStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_MyStruct(ctx, op, nil /* fldPath */, obj.(*MyStruct), safe.Cast[*MyStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type MyStructBeta + scheme.AddValidationFunc((*MyStructBeta)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_MyStructBeta(ctx, op, nil /* fldPath */, obj.(*MyStructBeta), safe.Cast[*MyStructBeta](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type Struct + 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()))} + }) + // type UnionStructBeta + scheme.AddValidationFunc((*UnionStructBeta)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_UnionStructBeta(ctx, op, nil /* fldPath */, obj.(*UnionStructBeta), safe.Cast[*UnionStructBeta](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_levels_unions_MyStruct_ = validate.NewUnionMembership(validate.NewUnionMember("z1"), validate.NewUnionMember("z2")) + +// Validate_MyStruct validates an instance of MyStruct according +// to declarative validation rules in the API schema. +func Validate_MyStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MyStruct) (errs field.ErrorList) { + errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_MyStruct_, func(obj *MyStruct) bool { + if obj == nil { + return false + } + return obj.Z1 != nil + }, func(obj *MyStruct) bool { + if obj == nil { + return false + } + return obj.Z2 != nil + }).MarkAlpha()...) + + // field MyStruct.TypeMeta has no validation + + // field MyStruct.Z1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Z1, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("z1"), obj.Z1, safe.Field(oldObj, func(oldObj *MyStruct) *Z1 { return oldObj.Z1 }), oldObj != nil)...) + + // field MyStruct.Z2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *Z2, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("z2"), obj.Z2, safe.Field(oldObj, func(oldObj *MyStruct) *Z2 { return oldObj.Z2 }), oldObj != nil)...) + + return errs +} + +var zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_MyStructBeta_ = validate.NewUnionMembership(validate.NewUnionMember("z1Beta"), validate.NewUnionMember("z2Beta")) + +// Validate_MyStructBeta validates an instance of MyStructBeta according +// to declarative validation rules in the API schema. +func Validate_MyStructBeta(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MyStructBeta) (errs field.ErrorList) { + errs = append(errs, validate.ZeroOrOneOfUnion(ctx, op, fldPath, obj, oldObj, zeroOrOneOfMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_MyStructBeta_, func(obj *MyStructBeta) bool { + if obj == nil { + return false + } + return obj.Z1Beta != nil + }, func(obj *MyStructBeta) bool { + if obj == nil { + return false + } + return obj.Z2Beta != nil + }).MarkBeta()...) + + // field MyStructBeta.TypeMeta has no validation + + // field MyStructBeta.Z1Beta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *BetaZ1, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("z1Beta"), obj.Z1Beta, safe.Field(oldObj, func(oldObj *MyStructBeta) *BetaZ1 { return oldObj.Z1Beta }), oldObj != nil)...) + + // field MyStructBeta.Z2Beta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *BetaZ2, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("z2Beta"), obj.Z2Beta, safe.Field(oldObj, func(oldObj *MyStructBeta) *BetaZ2 { return oldObj.Z2Beta }), oldObj != nil)...) + + return errs +} + +var unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_Struct_ = validate.NewDiscriminatedUnionMembership("d", validate.NewDiscriminatedUnionMember("m1", "M1"), validate.NewDiscriminatedUnionMember("m2", "M2")) + +// Validate_Struct validates an instance of Struct according +// to declarative validation rules in the API schema. +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + errs = append(errs, validate.DiscriminatedUnion(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_Struct_, func(obj *Struct) string { + if obj == nil { + return "" + } + return string(obj.D) + }, 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 + }).MarkAlpha()...) + + // field Struct.TypeMeta has no validation + // field Struct.D has no validation + + // field Struct.M1 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *M1, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("m1"), obj.M1, safe.Field(oldObj, func(oldObj *Struct) *M1 { return oldObj.M1 }), oldObj != nil)...) + + // field Struct.M2 + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *M2, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("m2"), obj.M2, safe.Field(oldObj, func(oldObj *Struct) *M2 { return oldObj.M2 }), oldObj != nil)...) + + return errs +} + +var unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_UnionStructBeta_ = validate.NewDiscriminatedUnionMembership("dBeta", validate.NewDiscriminatedUnionMember("m1Beta", "M1Beta"), validate.NewDiscriminatedUnionMember("m2Beta", "M2Beta")) + +// Validate_UnionStructBeta validates an instance of UnionStructBeta according +// to declarative validation rules in the API schema. +func Validate_UnionStructBeta(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *UnionStructBeta) (errs field.ErrorList) { + errs = append(errs, validate.DiscriminatedUnion(ctx, op, fldPath, obj, oldObj, unionMembershipFor_k8s_io_code_generator_cmd_validation_gen_output_tests_tags_levels_unions_UnionStructBeta_, func(obj *UnionStructBeta) string { + if obj == nil { + return "" + } + return string(obj.DBeta) + }, func(obj *UnionStructBeta) bool { + if obj == nil { + return false + } + return obj.M1Beta != nil + }, func(obj *UnionStructBeta) bool { + if obj == nil { + return false + } + return obj.M2Beta != nil + }).MarkBeta()...) + + // field UnionStructBeta.TypeMeta has no validation + // field UnionStructBeta.DBeta has no validation + + // field UnionStructBeta.M1Beta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *BetaM1, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("m1Beta"), obj.M1Beta, safe.Field(oldObj, func(oldObj *UnionStructBeta) *BetaM1 { return oldObj.M1Beta }), oldObj != nil)...) + + // field UnionStructBeta.M2Beta + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *BetaM2, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) { + return nil + } + // call field-attached validations + earlyReturn := false + if e := validate.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + earlyReturn = true + } + if earlyReturn { + return // do not proceed + } + return + }(fldPath.Child("m2Beta"), obj.M2Beta, safe.Field(oldObj, func(oldObj *UnionStructBeta) *BetaM2 { return oldObj.M2Beta }), oldObj != nil)...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc.go new file mode 100644 index 00000000000..4e4398052ef --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc.go @@ -0,0 +1,36 @@ +/* +Copyright 2025 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 + +package uniquetag + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type UniqueStruct struct { + TypeMeta int + + // +k8s:listType=atomic + // +k8s:alpha=+k8s:unique=set + AlphaUniqueSet []string `json:"alphaUniqueSet"` + + // +k8s:listType=atomic + // +k8s:beta=+k8s:unique=set + BetaUniqueSet []string `json:"betaUniqueSet"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc_test.go new file mode 100644 index 00000000000..a37747ed01f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/doc_test.go @@ -0,0 +1,35 @@ +/* +Copyright 2025 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 uniquetag + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestMisc(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&UniqueStruct{ + AlphaUniqueSet: []string{"a", "a"}, + BetaUniqueSet: []string{"a", "a"}, + }).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByOrigin().ByValidationStabilityLevel(), field.ErrorList{ + field.Duplicate(field.NewPath("alphaUniqueSet").Index(1), "a").MarkAlpha(), + field.Duplicate(field.NewPath("betaUniqueSet").Index(1), "a").MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/zz_generated.validations.go new file mode 100644 index 00000000000..d9f3bcf66c3 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/uniquetag/zz_generated.validations.go @@ -0,0 +1,84 @@ +//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 uniquetag + +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 { + // type UniqueStruct + scheme.AddValidationFunc((*UniqueStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_UniqueStruct(ctx, op, nil /* fldPath */, obj.(*UniqueStruct), safe.Cast[*UniqueStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_UniqueStruct validates an instance of UniqueStruct according +// to declarative validation rules in the API schema. +func Validate_UniqueStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *UniqueStruct) (errs field.ErrorList) { + // field UniqueStruct.TypeMeta has no validation + + // field UniqueStruct.AlphaUniqueSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkAlpha()...) + return + }(fldPath.Child("alphaUniqueSet"), obj.AlphaUniqueSet, safe.Field(oldObj, func(oldObj *UniqueStruct) []string { return oldObj.AlphaUniqueSet }), oldObj != nil)...) + + // field UniqueStruct.BetaUniqueSet + errs = append(errs, + func(fldPath *field.Path, obj, oldObj []string, oldValueCorrelated bool) (errs field.ErrorList) { + // don't revalidate unchanged data + if oldValueCorrelated && op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) { + return nil + } + // call field-attached validations + // lists with set semantics require unique values + errs = append(errs, validate.Unique(ctx, op, fldPath, obj, oldObj, validate.DirectEqual).MarkBeta()...) + return + }(fldPath.Child("betaUniqueSet"), obj.BetaUniqueSet, safe.Field(oldObj, func(oldObj *UniqueStruct) []string { return oldObj.BetaUniqueSet }), oldObj != nil)...) + + return errs +}