diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc.go new file mode 100644 index 00000000000..0f509d4c1d4 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc.go @@ -0,0 +1,103 @@ +/* +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. +*/ + +// +k8s:validation-gen=TypeMeta +// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme + +// +k8s:validation-gen-nolint +package discriminators + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type AlphaStruct struct { + TypeMeta int + + // +k8s:alpha=+k8s:discriminator + D1 string `json:"d1"` + + // +k8s:alpha=+k8s:member("A")=+k8s:required + FieldA *string `json:"fieldA,omitempty"` + + // +k8s:alpha=+k8s:member("B")=+k8s:required + FieldB *string `json:"fieldB,omitempty"` +} + +type BetaStruct struct { + TypeMeta int + + // +k8s:beta=+k8s:discriminator + D1 string `json:"d1"` + + // +k8s:beta=+k8s:member("A")=+k8s:required + FieldA *string `json:"fieldA,omitempty"` + + // +k8s:beta=+k8s:member("B")=+k8s:required + FieldB *string `json:"fieldB,omitempty"` +} + +type MixedLevels struct { + TypeMeta int + + // +k8s:discriminator + Mode string `json:"mode"` + + // +k8s:alpha=+k8s:member("A")=+k8s:required + A *string `json:"a,omitempty"` + + // +k8s:beta=+k8s:member("B")=+k8s:required + B *string `json:"b,omitempty"` +} + +type CrossLevels struct { + TypeMeta int + + // +k8s:beta=+k8s:discriminator + Kind string `json:"kind"` + + // +k8s:alpha=+k8s:member("A")=+k8s:required + A *string `json:"a,omitempty"` + + // +k8s:alpha=+k8s:member("B")=+k8s:required + B *string `json:"b,omitempty"` +} + +type SameFieldMixed struct { + TypeMeta int + + // +k8s:discriminator + Mode string `json:"mode"` + + // +k8s:alpha=+k8s:member("A")=+k8s:required + // +k8s:beta=+k8s:member("B")=+k8s:required + Value *string `json:"value,omitempty"` +} + +// SameValueMixedPayloads tests that multiple payload validations on the same +// discriminator value can have different stability levels. +type SameValueMixedPayloads struct { + TypeMeta int + + // +k8s:discriminator + Mode string `json:"mode"` + + // +k8s:alpha=+k8s:member("A")=+k8s:required + // +k8s:beta=+k8s:member("A")=+k8s:minLength=3 + Value *string `json:"value,omitempty"` +} + +type TypeMeta int diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc_test.go new file mode 100644 index 00000000000..f9509dfe6a2 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/doc_test.go @@ -0,0 +1,162 @@ +/* +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. +*/ + +package discriminators + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +func TestAlpha(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid: mode A with FieldA set + st.Value(&AlphaStruct{D1: "A", FieldA: ptr.To("val")}).ExpectValid() + + // Invalid: mode A with FieldA missing (required), should be stability level alpha + st.Value(&AlphaStruct{D1: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("fieldA"), "").MarkAlpha(), + }) + + // Invalid: mode A with FieldB set (forbidden), should be stability level alpha + st.Value(&AlphaStruct{D1: "A", FieldA: ptr.To("val"), FieldB: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Forbidden(field.NewPath("fieldB"), "").MarkAlpha(), + }) + + // Valid: mode B with FieldB set + st.Value(&AlphaStruct{D1: "B", FieldB: ptr.To("val")}).ExpectValid() + + // Invalid: mode B with FieldB missing (required), should be stability level alpha + st.Value(&AlphaStruct{D1: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("fieldB"), "").MarkAlpha(), + }) +} + +func TestBeta(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid: mode A with FieldA set + st.Value(&BetaStruct{D1: "A", FieldA: ptr.To("val")}).ExpectValid() + + // Invalid: mode A with FieldA missing (required), should be stability level beta + st.Value(&BetaStruct{D1: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("fieldA"), "").MarkBeta(), + }) + + // Invalid: mode A with FieldB set (forbidden), should be stability level beta + st.Value(&BetaStruct{D1: "A", FieldA: ptr.To("val"), FieldB: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Forbidden(field.NewPath("fieldB"), "").MarkBeta(), + }) + + // Valid: mode B with FieldB set + st.Value(&BetaStruct{D1: "B", FieldB: ptr.To("val")}).ExpectValid() + + // Invalid: mode B with FieldB missing (required), should be stability level beta + st.Value(&BetaStruct{D1: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("fieldB"), "").MarkBeta(), + }) +} + +func TestMixedLevels(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid cases + st.Value(&MixedLevels{Mode: "A", A: ptr.To("val")}).ExpectValid() + st.Value(&MixedLevels{Mode: "B", B: ptr.To("val")}).ExpectValid() + + // Mode=A, missing A -> alpha error (field A is alpha-gated) + st.Value(&MixedLevels{Mode: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("a"), "").MarkAlpha(), + }) + + // Mode=B, missing B -> beta error (field B is beta-gated) + st.Value(&MixedLevels{Mode: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("b"), "").MarkBeta(), + }) + + // Mode=A with B set (forbidden) -> beta error (field B's +k8s:discriminator is beta) + st.Value(&MixedLevels{Mode: "A", A: ptr.To("val"), B: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Forbidden(field.NewPath("b"), "").MarkBeta(), + }) + + // Mode=B with A set (forbidden) -> alpha error (field A's +k8s:discriminator is alpha) + st.Value(&MixedLevels{Mode: "B", A: ptr.To("val"), B: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Forbidden(field.NewPath("a"), "").MarkAlpha(), + }) +} + +func TestCrossLevels(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid cases + st.Value(&CrossLevels{Kind: "A", A: ptr.To("val")}).ExpectValid() + st.Value(&CrossLevels{Kind: "B", B: ptr.To("val")}).ExpectValid() + + // Kind=A, missing A -> alpha error (field A is alpha-gated, discriminator is beta) + st.Value(&CrossLevels{Kind: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("a"), "").MarkAlpha(), + }) + + // Kind=B, missing B -> alpha error (field B is alpha-gated, discriminator is beta) + st.Value(&CrossLevels{Kind: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("b"), "").MarkAlpha(), + }) + + // Kind=A with B set (forbidden) -> alpha error (field B's member tag is alpha) + st.Value(&CrossLevels{Kind: "A", A: ptr.To("val"), B: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Forbidden(field.NewPath("b"), "").MarkAlpha(), + }) +} + +func TestSameFieldMixed(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid cases + st.Value(&SameFieldMixed{Mode: "A", Value: ptr.To("val")}).ExpectValid() + st.Value(&SameFieldMixed{Mode: "B", Value: ptr.To("val")}).ExpectValid() + + // Mode=A, missing Value -> alpha error (member("A") is alpha-gated) + st.Value(&SameFieldMixed{Mode: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("value"), "").MarkAlpha(), + }) + + // Mode=B, missing Value -> beta error (member("B") is beta-gated) + st.Value(&SameFieldMixed{Mode: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("value"), "").MarkBeta(), + }) +} + +// TestSameValueMixedPayloads verifies that multiple payload validations on +// the same discriminator value can have different stability levels. +func TestSameValueMixedPayloads(t *testing.T) { + st := localSchemeBuilder.Test(t) + + // Valid: Mode=A with Value set and long enough + st.Value(&SameValueMixedPayloads{Mode: "A", Value: ptr.To("abc")}).ExpectValid() + + // Mode=A, missing Value -> alpha error (required is alpha-gated) + st.Value(&SameValueMixedPayloads{Mode: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.Required(field.NewPath("value"), "").MarkAlpha(), + }) + + // Mode=A, Value too short -> beta error (minLength is beta-gated) + st.Value(&SameValueMixedPayloads{Mode: "A", Value: ptr.To("ab")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByValidationStabilityLevel(), field.ErrorList{ + field.TooShort(field.NewPath("value"), "", 3).MarkBeta(), + }) +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/zz_generated.validations.go new file mode 100644 index 00000000000..0cd8b45295c --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/levels/discriminators/zz_generated.validations.go @@ -0,0 +1,357 @@ +//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 discriminators + +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 AlphaStruct + scheme.AddValidationFunc((*AlphaStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_AlphaStruct(ctx, op, nil /* fldPath */, obj.(*AlphaStruct), safe.Cast[*AlphaStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type BetaStruct + scheme.AddValidationFunc((*BetaStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_BetaStruct(ctx, op, nil /* fldPath */, obj.(*BetaStruct), safe.Cast[*BetaStruct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type CrossLevels + scheme.AddValidationFunc((*CrossLevels)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_CrossLevels(ctx, op, nil /* fldPath */, obj.(*CrossLevels), safe.Cast[*CrossLevels](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type MixedLevels + scheme.AddValidationFunc((*MixedLevels)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_MixedLevels(ctx, op, nil /* fldPath */, obj.(*MixedLevels), safe.Cast[*MixedLevels](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type SameFieldMixed + scheme.AddValidationFunc((*SameFieldMixed)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_SameFieldMixed(ctx, op, nil /* fldPath */, obj.(*SameFieldMixed), safe.Cast[*SameFieldMixed](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + // type SameValueMixedPayloads + scheme.AddValidationFunc((*SameValueMixedPayloads)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList { + switch op.Request.SubresourcePath() { + case "/": + return Validate_SameValueMixedPayloads(ctx, op, nil /* fldPath */, obj.(*SameValueMixedPayloads), safe.Cast[*SameValueMixedPayloads](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))} + }) + return nil +} + +// Validate_AlphaStruct validates an instance of AlphaStruct according +// to declarative validation rules in the API schema. +func Validate_AlphaStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *AlphaStruct) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *AlphaStruct) *string { return obj.FieldA }, func(obj *AlphaStruct) string { return obj.D1 }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "fieldB", func(obj *AlphaStruct) *string { return obj.FieldB }, func(obj *AlphaStruct) string { return obj.D1 }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "B", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + + // field AlphaStruct.TypeMeta has no validation + // field AlphaStruct.D1 has no validation + // field AlphaStruct.FieldA has no validation + // field AlphaStruct.FieldB has no validation + return errs +} + +// Validate_BetaStruct validates an instance of BetaStruct according +// to declarative validation rules in the API schema. +func Validate_BetaStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *BetaStruct) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *BetaStruct) *string { return obj.FieldA }, func(obj *BetaStruct) string { return obj.D1 }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkBeta()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "fieldB", func(obj *BetaStruct) *string { return obj.FieldB }, func(obj *BetaStruct) string { return obj.D1 }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkBeta()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "B", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + + // field BetaStruct.TypeMeta has no validation + // field BetaStruct.D1 has no validation + // field BetaStruct.FieldA has no validation + // field BetaStruct.FieldB has no validation + return errs +} + +// Validate_CrossLevels validates an instance of CrossLevels according +// to declarative validation rules in the API schema. +func Validate_CrossLevels(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *CrossLevels) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "a", func(obj *CrossLevels) *string { return obj.A }, func(obj *CrossLevels) string { return obj.Kind }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "b", func(obj *CrossLevels) *string { return obj.B }, func(obj *CrossLevels) string { return obj.Kind }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "B", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + + // field CrossLevels.TypeMeta has no validation + // field CrossLevels.Kind has no validation + // field CrossLevels.A has no validation + // field CrossLevels.B has no validation + return errs +} + +// Validate_MixedLevels validates an instance of MixedLevels according +// to declarative validation rules in the API schema. +func Validate_MixedLevels(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MixedLevels) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "a", func(obj *MixedLevels) *string { return obj.A }, func(obj *MixedLevels) string { return obj.Mode }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "b", func(obj *MixedLevels) *string { return obj.B }, func(obj *MixedLevels) string { return obj.Mode }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj).MarkBeta()...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "B", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + + // field MixedLevels.TypeMeta has no validation + // field MixedLevels.Mode has no validation + // field MixedLevels.A has no validation + // field MixedLevels.B has no validation + return errs +} + +// Validate_SameFieldMixed validates an instance of SameFieldMixed according +// to declarative validation rules in the API schema. +func Validate_SameFieldMixed(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SameFieldMixed) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "value", func(obj *SameFieldMixed) *string { return obj.Value }, func(obj *SameFieldMixed) string { return obj.Mode }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj)...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + { + Value: "B", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkBeta(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + return errs + }}, + })...) + + // field SameFieldMixed.TypeMeta has no validation + // field SameFieldMixed.Mode has no validation + // field SameFieldMixed.Value has no validation + return errs +} + +// Validate_SameValueMixedPayloads validates an instance of SameValueMixedPayloads according +// to declarative validation rules in the API schema. +func Validate_SameValueMixedPayloads(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SameValueMixedPayloads) (errs field.ErrorList) { + errs = append(errs, validate.Discriminated(ctx, op, fldPath, obj, oldObj, "value", func(obj *SameValueMixedPayloads) *string { return obj.Value }, func(obj *SameValueMixedPayloads) string { return obj.Mode }, validate.DirectEqualPtr, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + errs = append(errs, validate.ForbiddenPointer(ctx, op, fldPath, obj, oldObj)...) + return errs + }, []validate.DiscriminatedRule[*string, string]{ + { + Value: "A", Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList { + errs := field.ErrorList{} + earlyReturn := false + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj).MarkAlpha(); len(e) != 0 { + errs = append(errs, e...) + earlyReturn = true + } + if earlyReturn { + return errs + } + errs = append(errs, validate.MinLength(ctx, op, fldPath, obj, oldObj, 3).MarkBeta()...) + return errs + }}, + })...) + + // field SameValueMixedPayloads.TypeMeta has no validation + // field SameValueMixedPayloads.Mode has no validation + // field SameValueMixedPayloads.Value has no validation + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/discriminator.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/discriminator.go index 49d5dde3bcb..517f34ccaac 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/discriminator.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/discriminator.go @@ -63,8 +63,9 @@ type fieldMemberRules struct { } type memberRule struct { - value string - validations Validations + value string + validations Validations + stabilityLevel ValidationStabilityLevel } func (mg discriminatorGroups) getOrCreate(name string) *discriminatorGroup { @@ -204,8 +205,9 @@ func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag) } group.members[fieldName].rules = append(group.members[fieldName].rules, memberRule{ - value: value, - validations: payloadValidations, + value: value, + validations: payloadValidations, + stabilityLevel: context.StabilityLevel, }) return Validations{}, nil @@ -323,27 +325,53 @@ func (mtfv *discriminatorFieldValidator) generateMemberFieldValidation(context C } // Prepare DiscriminatedRules - // Aggregate rules by value - rulesByValue := make(map[string]Validations) + // Mark each rule's validation functions with their stability level before + // aggregating by value, so that different rules for the same value can + // carry different stability levels. + rulesByValue := make(map[string]*Validations) var values []string for _, rule := range rules.rules { + // Track unique discriminator values in order of first appearance. if _, ok := rulesByValue[rule.value]; !ok { + rulesByValue[rule.value] = &Validations{} values = append(values, rule.value) } - v := rulesByValue[rule.value] - v.Add(rule.validations) - rulesByValue[rule.value] = v + // Mark each validation function with its stability level before merging. + v := rule.validations + if rule.stabilityLevel != "" { + marked := make([]FunctionGen, len(v.Functions)) + for i, f := range v.Functions { + marked[i] = f.WithStabilityLevel(rule.stabilityLevel) + } + v.Functions = marked + } + // Accumulate this rule's validations with others that share the same discriminator value. + rulesByValue[rule.value].Add(v) } slices.Sort(values) discriminatorType := group.discriminatorMember.Type + // When all rules share the same stability level, the default-forbidden + // (which fires for unrecognized discriminator values) should also be marked + // with that level so its errors carry the same stability annotation. + if uniformLevel := uniformStabilityLevel(rules.rules); uniformLevel != "" { + mwf, ok := defaultForbidden.(MultiWrapperFunction) + if !ok { + return Validations{}, fmt.Errorf("internal error: defaultForbidden is not a MultiWrapperFunction") + } + marked := make([]FunctionGen, len(mwf.Functions)) + for i, f := range mwf.Functions { + marked[i] = f.WithStabilityLevel(uniformLevel) + } + mwf.Functions = marked + defaultForbidden = mwf + } + var discriminatedRules []any for _, val := range values { - ruleValidations := rulesByValue[val] - wrapper := MultiWrapperFunction{ - Functions: ruleValidations.Functions, + Functions: rulesByValue[val].Functions, ObjType: nilableFieldType, } @@ -408,6 +436,18 @@ func (mtfv *discriminatorFieldValidator) generateMemberFieldValidation(context C return Validations{Functions: []FunctionGen{fn}}, nil } +// uniformStabilityLevel returns the common stability level if all rules share +// the same one, or "" if they differ. +func uniformStabilityLevel(rules []memberRule) ValidationStabilityLevel { + level := rules[0].stabilityLevel + for i := 1; i < len(rules); i++ { + if rules[i].stabilityLevel != level { + return "" + } + } + return level +} + func (mtfv *discriminatorFieldValidator) getForbiddenValidation(t *types.Type) (any, error) { var forbidden types.Name nt := util.NativeType(t)