Implement declarative modal validation (+k8s:discriminator and +k8s:member)

This introduces state-based validation to declarative validation via the +k8s:discriminator and +k8s:member tags.

Key changes:
- Added validate.Modal runtime function to handle state-based logic, including full ratcheting support (skipping validation on Update if discriminator and value are unchanged).
- Implemented +k8s:discriminator to identify the controlling field (supports string, int, and bool).
- Implemented +k8s:member to map discriminator values to validation rules.
- Added implicit "forbidden-by-default" logic for unhandled modes, with support for pointer, slice, and map types.
This commit is contained in:
Aaron Prindle
2026-02-06 03:02:10 +00:00
parent d59cfc969d
commit c04c60f408
6 changed files with 1467 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
/*
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 validate
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ModalRule defines a validation to apply for a specific mode value.
type ModalRule[T any] struct {
Value string
Validation ValidateFunc[T]
}
// Modal validates a value based on a discriminator value.
// It iterates through the rules and applies the first one that matches the discriminator.
// If no rule matches, it applies the defaultValidation if provided.
//
// It performs ratcheting: if the operation is an Update, and neither the discriminator
// nor the value (checked via equiv) have changed, validation is skipped.
func Modal[T any, D comparable, P any](ctx context.Context, op operation.Operation, structPath *field.Path,
obj, oldObj *P, fieldName string, getMemberValue func(*P) T, getDiscriminator func(*P) D,
equiv MatchFunc[T], defaultValidation ValidateFunc[T], rules []ModalRule[T],
) field.ErrorList {
value := getMemberValue(obj)
var oldValue T
var oldDiscriminator D
if oldObj != nil {
oldValue = getMemberValue(oldObj)
oldDiscriminator = getDiscriminator(oldObj)
}
discriminator := getDiscriminator(obj)
if op.Type == operation.Update && oldObj != nil && discriminator == oldDiscriminator && equiv(value, oldValue) {
return nil
}
fldPath := structPath.Child(fieldName)
dStr := fmt.Sprintf("%v", discriminator)
for _, rule := range rules {
if rule.Value == dStr {
if rule.Validation == nil {
return nil
}
return rule.Validation(ctx, op, fldPath, value, oldValue)
}
}
if defaultValidation != nil {
return defaultValidation(ctx, op, fldPath, value, oldValue)
}
return nil
}

View File

@@ -0,0 +1,224 @@
/*
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 validate
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestModal(t *testing.T) {
errMatch := field.ErrorList{field.Invalid(field.NewPath("foo"), "bar", "match error")}
errDefault := field.ErrorList{field.Invalid(field.NewPath("foo"), "bar", "default error")}
mockValid := func(_ context.Context, _ operation.Operation, _ *field.Path, _, _ *string) field.ErrorList {
return nil
}
mockErrorMatch := func(_ context.Context, _ operation.Operation, _ *field.Path, _, _ *string) field.ErrorList {
return errMatch
}
mockErrorDefault := func(_ context.Context, _ operation.Operation, _ *field.Path, _, _ *string) field.ErrorList {
return errDefault
}
mockEqual := func(a, b *string) bool {
return a == b
}
testCases := []struct {
name string
opType operation.Type
discriminator any
oldDiscriminator any
value *string
oldValue *string
rules []ModalRule[*string]
defaultValidation ValidateFunc[*string]
expected field.ErrorList
}{
{
name: "matches rule, returns valid",
opType: operation.Create,
discriminator: "A",
oldDiscriminator: "A",
rules: []ModalRule[*string]{
{Value: "A", Validation: mockValid},
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: nil,
},
{
name: "matches rule, returns error",
opType: operation.Create,
discriminator: "B",
oldDiscriminator: "B",
rules: []ModalRule[*string]{
{Value: "A", Validation: mockValid},
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
{
name: "ratcheting: update, unchanged, skips validation",
opType: operation.Update,
discriminator: "B",
oldDiscriminator: "B", // unchanged
value: nil,
oldValue: nil, // unchanged
rules: []ModalRule[*string]{
{Value: "B", Validation: mockErrorMatch}, // would fail if run
},
defaultValidation: mockErrorDefault,
expected: nil,
},
{
name: "ratcheting: update, discriminator changed, runs validation",
opType: operation.Update,
discriminator: "B",
oldDiscriminator: "A", // changed
value: nil,
oldValue: nil,
rules: []ModalRule[*string]{
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
{
name: "matches rule with nil validation, returns valid",
opType: operation.Create,
discriminator: "A",
rules: []ModalRule[*string]{
{Value: "A", Validation: nil},
},
defaultValidation: mockErrorDefault,
expected: nil,
},
{
name: "no match, runs default",
opType: operation.Create,
discriminator: "C",
rules: []ModalRule[*string]{
{Value: "A", Validation: mockValid},
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errDefault,
},
{
name: "no match, nil default, returns valid",
opType: operation.Create,
discriminator: "C",
rules: []ModalRule[*string]{
{Value: "A", Validation: mockValid},
},
defaultValidation: nil,
expected: nil,
},
{
name: "int discriminator matches",
opType: operation.Create,
discriminator: 1,
rules: []ModalRule[*string]{
{Value: "1", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
{
name: "bool discriminator matches",
opType: operation.Create,
discriminator: true,
rules: []ModalRule[*string]{
{Value: "true", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Handle nil oldDiscriminator for tests that don't specify it
oldDisc := tc.oldDiscriminator
if oldDisc == nil {
oldDisc = tc.discriminator
}
// We need separate Modal calls because D is different per test case
var got field.ErrorList
switch d := tc.discriminator.(type) {
case int:
od, _ := oldDisc.(int)
type IntP struct {
Val *string
Disc int
}
newObj := &IntP{Val: tc.value, Disc: d}
var oldObj *IntP
if tc.opType == operation.Update {
oldObj = &IntP{Val: tc.oldValue, Disc: od}
}
getVal := func(p *IntP) *string { return p.Val }
getDisc := func(p *IntP) int { return p.Disc }
got = Modal[*string, int, IntP](context.Background(), operation.Operation{Type: tc.opType}, field.NewPath("root"), newObj, oldObj, "field", getVal, getDisc, mockEqual, tc.defaultValidation, tc.rules)
case bool:
od, _ := oldDisc.(bool)
type BoolP struct {
Val *string
Disc bool
}
newObj := &BoolP{Val: tc.value, Disc: d}
var oldObj *BoolP
if tc.opType == operation.Update {
oldObj = &BoolP{Val: tc.oldValue, Disc: od}
}
getVal := func(p *BoolP) *string { return p.Val }
getDisc := func(p *BoolP) bool { return p.Disc }
got = Modal[*string, bool, BoolP](context.Background(), operation.Operation{Type: tc.opType}, field.NewPath("root"), newObj, oldObj, "field", getVal, getDisc, mockEqual, tc.defaultValidation, tc.rules)
case string:
od, _ := oldDisc.(string)
type StringP struct {
Val *string
Disc string
}
newObj := &StringP{Val: tc.value, Disc: d}
var oldObj *StringP
if tc.opType == operation.Update {
oldObj = &StringP{Val: tc.oldValue, Disc: od}
}
getVal := func(p *StringP) *string { return p.Val }
getDisc := func(p *StringP) string { return p.Disc }
got = Modal[*string, string, StringP](context.Background(), operation.Operation{Type: tc.opType}, field.NewPath("root"), newObj, oldObj, "field", getVal, getDisc, mockEqual, tc.defaultValidation, tc.rules)
}
if !reflect.DeepEqual(got, tc.expected) {
t.Errorf("got %v want %v", got, tc.expected)
}
})
}
}

View File

@@ -0,0 +1,120 @@
/*
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
// This is a test package.
package mode
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
type StrictUnion struct {
TypeMeta int
// +k8s:discriminator
D1 string `json:"d1"`
// +k8s:member("A")=+k8s:required
FieldA *string `json:"fieldA,omitempty"`
// +k8s:member("B")=+k8s:required
FieldB *string `json:"fieldB,omitempty"`
}
type SharedField struct {
TypeMeta int
// +k8s:discriminator
D1 string `json:"d1"`
// Valid in A and B, implicitly forbidden in C.
// +k8s:member("A")=+k8s:optional
// +k8s:member("B")=+k8s:optional
FieldA *string `json:"fieldA,omitempty"`
}
type ChainedValidation struct {
TypeMeta int
// +k8s:discriminator
D1 string `json:"d1"`
// In mode A, it is required AND must have maxLength 5.
// +k8s:member("A")=+k8s:required
// +k8s:member("A")=+k8s:maxLength=5
FieldA *string `json:"fieldA,omitempty"`
}
type ImplicitForbidden struct {
TypeMeta int
// +k8s:discriminator
D1 string `json:"d1"`
// Field is only mentioned for mode A. Mode B should implicitly forbid it.
// +k8s:member("A")=+k8s:optional
FieldA *string `json:"fieldA,omitempty"`
}
type NonStringDiscriminator struct {
TypeMeta int
// +k8s:discriminator(name:"Bool")
D1 bool `json:"d1"`
// +k8s:member(discriminator:"Bool", value:"true")=+k8s:required
FieldA *string `json:"fieldA,omitempty"`
// +k8s:discriminator(name:"Int")
D2 int `json:"d2"`
// +k8s:member(discriminator:"Int", value:"1")=+k8s:required
FieldB *string `json:"fieldB,omitempty"`
}
type MultipleDiscriminators struct {
TypeMeta int
// +k8s:discriminator(name:"D1")
D1 string `json:"d1"`
// +k8s:discriminator(name:"D2")
D2 string `json:"d2"`
// +k8s:member(discriminator:"D1", value:"A")=+k8s:required
FieldA *string `json:"fieldA,omitempty"`
// +k8s:member(discriminator:"D2", value:"B")=+k8s:required
FieldB *string `json:"fieldB,omitempty"`
}
type Collections struct {
TypeMeta int
// +k8s:discriminator
D1 string `json:"d1"`
// +k8s:member("A")=+k8s:optional
ListField []string `json:"listField,omitempty"`
// +k8s:member("A")=+k8s:optional
MapField map[string]string `json:"mapField,omitempty"`
}
type TypeMeta int

View File

@@ -0,0 +1,204 @@
/*
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 mode
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestStrictUnion(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Mode A: FieldA required, FieldB implicitly forbidden
st.Value(&StrictUnion{D1: "A", FieldA: ptr.To("val")}).ExpectValid()
st.Value(&StrictUnion{D1: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldA"), ""),
})
st.Value(&StrictUnion{D1: "A", FieldA: ptr.To("val"), FieldB: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldB"), ""),
})
// Mode B: FieldA implicitly forbidden, FieldB required
st.Value(&StrictUnion{D1: "B", FieldB: ptr.To("val")}).ExpectValid()
st.Value(&StrictUnion{D1: "B"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldB"), ""),
})
st.Value(&StrictUnion{D1: "B", FieldA: ptr.To("val"), FieldB: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldA"), ""),
})
}
func TestSharedField(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Valid (optional) in A and B
st.Value(&SharedField{D1: "A"}).ExpectValid()
st.Value(&SharedField{D1: "A", FieldA: ptr.To("val")}).ExpectValid()
st.Value(&SharedField{D1: "B"}).ExpectValid()
st.Value(&SharedField{D1: "B", FieldA: ptr.To("val")}).ExpectValid()
// Forbidden in C
st.Value(&SharedField{D1: "C", FieldA: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldA"), ""),
})
}
func TestChainedValidation(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Mode A: Required AND maxLength 5
st.Value(&ChainedValidation{D1: "A", FieldA: ptr.To("abc")}).ExpectValid()
st.Value(&ChainedValidation{D1: "A"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldA"), ""),
})
st.Value(&ChainedValidation{D1: "A", FieldA: ptr.To("too-long")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.TooLong(field.NewPath("fieldA"), "too-long", 5),
})
// Mode B: Unlisted, so implicitly forbidden
st.Value(&ChainedValidation{D1: "B", FieldA: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldA"), ""),
})
}
func TestImplicitForbidden(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Mode A: Optional
st.Value(&ImplicitForbidden{D1: "A"}).ExpectValid()
st.Value(&ImplicitForbidden{D1: "A", FieldA: ptr.To("val")}).ExpectValid()
// Mode B: Not listed, so implicitly Forbidden
st.Value(&ImplicitForbidden{D1: "B", FieldA: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldA"), ""),
})
}
func TestNonStringDiscriminator(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Bool mode
st.Value(&NonStringDiscriminator{D1: true, FieldA: ptr.To("val")}).ExpectValid()
st.Value(&NonStringDiscriminator{D1: true}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldA"), ""),
})
st.Value(&NonStringDiscriminator{D1: false, FieldA: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldA"), ""),
})
// Int mode
st.Value(&NonStringDiscriminator{D2: 1, FieldB: ptr.To("val")}).ExpectValid()
st.Value(&NonStringDiscriminator{D2: 1}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldB"), ""),
})
st.Value(&NonStringDiscriminator{D2: 2, FieldB: ptr.To("val")}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("fieldB"), ""),
})
}
func TestMultipleDiscriminators(t *testing.T) {
st := localSchemeBuilder.Test(t)
st.Value(&MultipleDiscriminators{
D1: "A",
D2: "B",
FieldA: ptr.To("valA"),
FieldB: ptr.To("valB"),
}).ExpectValid()
st.Value(&MultipleDiscriminators{
D1: "A",
D2: "B",
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Required(field.NewPath("fieldA"), ""),
field.Required(field.NewPath("fieldB"), ""),
})
}
func TestCollections(t *testing.T) {
st := localSchemeBuilder.Test(t)
// Mode A: Collections are valid (optional)
st.Value(&Collections{
D1: "A",
}).ExpectValid()
st.Value(&Collections{
D1: "A",
ListField: []string{"item"},
MapField: map[string]string{"key": "val"},
}).ExpectValid()
// Mode B: Unlisted, so implicitly forbidden
st.Value(&Collections{
D1: "B",
ListField: []string{"item"},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("listField"), ""),
})
st.Value(&Collections{
D1: "B",
MapField: map[string]string{"key": "val"},
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.Forbidden(field.NewPath("mapField"), ""),
})
}
func TestRatcheting(t *testing.T) {
mkTest := func() *ChainedValidation {
return &ChainedValidation{
D1: "A",
FieldA: ptr.To("too-long-string"),
}
}
st := localSchemeBuilder.Test(t)
// 1. New object is invalid
st.Value(mkTest()).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.TooLong(field.NewPath("fieldA"), "too-long-string", 5),
})
// 2. Unchanged update is valid (ratcheting)
st.Value(mkTest()).OldValue(mkTest()).ExpectValid()
// 3. Changed value re-validates (and fails)
mkDifferent := func() *ChainedValidation {
return &ChainedValidation{
D1: "A",
FieldA: ptr.To("also-too-long"),
}
}
st.Value(mkTest()).OldValue(mkDifferent()).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.TooLong(field.NewPath("fieldA"), "too-long-string", 5),
})
// 4. Changed discriminator re-validates (and fails)
mkDifferentDisc := func() *ChainedValidation {
return &ChainedValidation{
D1: "B", // Discriminator changed from B -> A
FieldA: ptr.To("too-long-string"),
}
}
st.Value(mkTest()).OldValue(mkDifferentDisc()).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
field.TooLong(field.NewPath("fieldA"), "too-long-string", 5),
})
}

View File

@@ -0,0 +1,415 @@
//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 mode
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 ChainedValidation
scheme.AddValidationFunc((*ChainedValidation)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_ChainedValidation(ctx, op, nil /* fldPath */, obj.(*ChainedValidation), safe.Cast[*ChainedValidation](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type Collections
scheme.AddValidationFunc((*Collections)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_Collections(ctx, op, nil /* fldPath */, obj.(*Collections), safe.Cast[*Collections](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type ImplicitForbidden
scheme.AddValidationFunc((*ImplicitForbidden)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_ImplicitForbidden(ctx, op, nil /* fldPath */, obj.(*ImplicitForbidden), safe.Cast[*ImplicitForbidden](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type MultipleDiscriminators
scheme.AddValidationFunc((*MultipleDiscriminators)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_MultipleDiscriminators(ctx, op, nil /* fldPath */, obj.(*MultipleDiscriminators), safe.Cast[*MultipleDiscriminators](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type NonStringDiscriminator
scheme.AddValidationFunc((*NonStringDiscriminator)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_NonStringDiscriminator(ctx, op, nil /* fldPath */, obj.(*NonStringDiscriminator), safe.Cast[*NonStringDiscriminator](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type SharedField
scheme.AddValidationFunc((*SharedField)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_SharedField(ctx, op, nil /* fldPath */, obj.(*SharedField), safe.Cast[*SharedField](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
// type StrictUnion
scheme.AddValidationFunc((*StrictUnion)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_StrictUnion(ctx, op, nil /* fldPath */, obj.(*StrictUnion), safe.Cast[*StrictUnion](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_ChainedValidation validates an instance of ChainedValidation according
// to declarative validation rules in the API schema.
func Validate_ChainedValidation(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ChainedValidation) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *ChainedValidation) *string { return obj.FieldA }, func(obj *ChainedValidation) 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)...)
return errs
}, []validate.ModalRule[*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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 5)...)
return errs
},
},
})...)
// field ChainedValidation.TypeMeta has no validation
// field ChainedValidation.D1 has no validation
// field ChainedValidation.FieldA has no validation
return errs
}
// Validate_Collections validates an instance of Collections according
// to declarative validation rules in the API schema.
func Validate_Collections(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Collections) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "listField", func(obj *Collections) []string { return obj.ListField }, func(obj *Collections) string { return obj.D1 }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj []string) field.ErrorList {
errs := field.ErrorList{}
errs = append(errs, validate.ForbiddenSlice(ctx, op, fldPath, obj, oldObj)...)
return errs
}, []validate.ModalRule[[]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.OptionalSlice(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "mapField", func(obj *Collections) map[string]string { return obj.MapField }, func(obj *Collections) string { return obj.D1 }, validate.SemanticDeepEqual, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj map[string]string) field.ErrorList {
errs := field.ErrorList{}
errs = append(errs, validate.ForbiddenMap(ctx, op, fldPath, obj, oldObj)...)
return errs
}, []validate.ModalRule[map[string]string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj map[string]string) field.ErrorList {
errs := field.ErrorList{}
earlyReturn := false
if e := validate.OptionalMap(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field Collections.TypeMeta has no validation
// field Collections.D1 has no validation
// field Collections.ListField has no validation
// field Collections.MapField has no validation
return errs
}
// Validate_ImplicitForbidden validates an instance of ImplicitForbidden according
// to declarative validation rules in the API schema.
func Validate_ImplicitForbidden(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ImplicitForbidden) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *ImplicitForbidden) *string { return obj.FieldA }, func(obj *ImplicitForbidden) 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)...)
return errs
}, []validate.ModalRule[*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.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field ImplicitForbidden.TypeMeta has no validation
// field ImplicitForbidden.D1 has no validation
// field ImplicitForbidden.FieldA has no validation
return errs
}
// Validate_MultipleDiscriminators validates an instance of MultipleDiscriminators according
// to declarative validation rules in the API schema.
func Validate_MultipleDiscriminators(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *MultipleDiscriminators) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *MultipleDiscriminators) *string { return obj.FieldA }, func(obj *MultipleDiscriminators) 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)...)
return errs
}, []validate.ModalRule[*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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldB", func(obj *MultipleDiscriminators) *string { return obj.FieldB }, func(obj *MultipleDiscriminators) string { return obj.D2 }, 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.ModalRule[*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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field MultipleDiscriminators.TypeMeta has no validation
// field MultipleDiscriminators.D1 has no validation
// field MultipleDiscriminators.D2 has no validation
// field MultipleDiscriminators.FieldA has no validation
// field MultipleDiscriminators.FieldB has no validation
return errs
}
// Validate_NonStringDiscriminator validates an instance of NonStringDiscriminator according
// to declarative validation rules in the API schema.
func Validate_NonStringDiscriminator(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *NonStringDiscriminator) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *NonStringDiscriminator) *string { return obj.FieldA }, func(obj *NonStringDiscriminator) bool { 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)...)
return errs
}, []validate.ModalRule[*string]{
{
Value: "true",
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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldB", func(obj *NonStringDiscriminator) *string { return obj.FieldB }, func(obj *NonStringDiscriminator) int { return obj.D2 }, 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.ModalRule[*string]{
{
Value: "1",
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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field NonStringDiscriminator.TypeMeta has no validation
// field NonStringDiscriminator.D1 has no validation
// field NonStringDiscriminator.FieldA has no validation
// field NonStringDiscriminator.D2 has no validation
// field NonStringDiscriminator.FieldB has no validation
return errs
}
// Validate_SharedField validates an instance of SharedField according
// to declarative validation rules in the API schema.
func Validate_SharedField(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *SharedField) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *SharedField) *string { return obj.FieldA }, func(obj *SharedField) 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)...)
return errs
}, []validate.ModalRule[*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.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
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.OptionalPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 {
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field SharedField.TypeMeta has no validation
// field SharedField.D1 has no validation
// field SharedField.FieldA has no validation
return errs
}
// Validate_StrictUnion validates an instance of StrictUnion according
// to declarative validation rules in the API schema.
func Validate_StrictUnion(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *StrictUnion) (errs field.ErrorList) {
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldA", func(obj *StrictUnion) *string { return obj.FieldA }, func(obj *StrictUnion) 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)...)
return errs
}, []validate.ModalRule[*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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
errs = append(errs, validate.Modal(ctx, op, fldPath, obj, oldObj, "fieldB", func(obj *StrictUnion) *string { return obj.FieldB }, func(obj *StrictUnion) 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)...)
return errs
}, []validate.ModalRule[*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); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return errs
}
return errs
},
},
})...)
// field StrictUnion.TypeMeta has no validation
// field StrictUnion.D1 has no validation
// field StrictUnion.FieldA has no validation
// field StrictUnion.FieldB has no validation
return errs
}

View File

@@ -0,0 +1,429 @@
/*
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 validators
import (
"fmt"
"slices"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/code-generator/cmd/validation-gen/util"
"k8s.io/gengo/v2/codetags"
"k8s.io/gengo/v2/parser/tags"
"k8s.io/gengo/v2/types"
)
const (
discriminatorTagName = "k8s:discriminator"
memberTagName = "k8s:member"
)
func init() {
RegisterTagValidator(&modeTagValidator{modeDefinitions})
RegisterTagValidator(&memberTagValidator{modeDefinitions, nil})
RegisterTypeValidator(&modeTypeOrFieldValidator{modeDefinitions})
RegisterFieldValidator(&modeTypeOrFieldValidator{modeDefinitions})
}
// modeDefinitions stores all mode definitions found by tag validators.
// Key is the struct path.
var modeDefinitions = map[string]modeGroups{}
type modeGroups map[string]*modeGroup
type modeGroup struct {
name string
discriminatorMember *types.Member
// members maps field names to their rules in this mode group.
members map[string]*fieldModeRules
}
type fieldModeRules struct {
member *types.Member
rules []modeRule
}
type modeRule struct {
value string
validations Validations
}
func (mg modeGroups) getOrCreate(name string) *modeGroup {
if name == "" {
name = "default"
}
g, ok := mg[name]
if !ok {
g = &modeGroup{
name: name,
members: make(map[string]*fieldModeRules),
}
mg[name] = g
}
return g
}
type modeTagValidator struct {
shared map[string]modeGroups
}
func (mtv *modeTagValidator) Init(_ Config) {}
func (mtv *modeTagValidator) TagName() string {
return discriminatorTagName
}
func (mtv *modeTagValidator) ValidScopes() sets.Set[Scope] {
return sets.New(ScopeField)
}
func (mtv *modeTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
if util.NativeType(context.Type).Kind == types.Pointer {
return Validations{}, fmt.Errorf("can only be used on non-pointer types")
}
if t := util.NonPointer(util.NativeType(context.Type)); t.Kind != types.Builtin || (t.Name.Name != "string" && t.Name.Name != "bool" && !types.IsInteger(t)) {
return Validations{}, fmt.Errorf("can only be used on string, bool or integer types (%s)", rootTypeString(context.Type, t))
}
if mtv.shared[context.ParentPath.String()] == nil {
mtv.shared[context.ParentPath.String()] = make(modeGroups)
}
modeName := ""
if nameArg, ok := tag.NamedArg("name"); ok {
modeName = nameArg.Value
}
group := mtv.shared[context.ParentPath.String()].getOrCreate(modeName)
if group.discriminatorMember != nil && group.discriminatorMember != context.Member {
return Validations{}, fmt.Errorf("duplicate discriminator: %q", modeName)
}
group.discriminatorMember = context.Member
return Validations{}, nil
}
func (mtv *modeTagValidator) Docs() TagDoc {
return TagDoc{
Tag: mtv.TagName(),
StabilityLevel: Alpha,
Scopes: mtv.ValidScopes().UnsortedList(),
Description: "Indicates that this field is a discriminator for state-based validation.",
Args: []TagArgDoc{{
Name: "name",
Description: "<string>",
Docs: "the name of the discriminator group, if more than one exists",
Type: codetags.ArgTypeString,
}},
}
}
type memberTagValidator struct {
shared map[string]modeGroups
validator Validator
}
func (mtv *memberTagValidator) Init(cfg Config) {
mtv.validator = cfg.Validator
}
func (mtv *memberTagValidator) TagName() string {
return memberTagName
}
func (mtv *memberTagValidator) ValidScopes() sets.Set[Scope] {
return sets.New(ScopeField)
}
var disallowedPayloadTags = sets.New(
listTypeTagName,
ListMapKeyTagName,
unionDiscriminatorTagName,
unionMemberTagName,
zeroOrOneOfMemberTagName,
discriminatorTagName,
memberTagName,
)
func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
if tag.ValueTag == nil {
return Validations{}, fmt.Errorf("missing required payload")
}
if disallowedPayloadTags.Has(tag.ValueTag.Name) {
return Validations{}, fmt.Errorf("unsupported payload tag: %q", tag.ValueTag.Name)
}
modeName := ""
if modeArg, ok := tag.NamedArg("discriminator"); ok {
modeName = modeArg.Value
}
value := ""
if valArg, ok := tag.NamedArg("value"); ok {
value = valArg.Value
} else if len(tag.Args) > 0 && tag.Args[0].Name == "" {
// Positional argument
value = tag.Args[0].Value
} else {
return Validations{}, fmt.Errorf("missing required value")
}
if mtv.shared[context.ParentPath.String()] == nil {
mtv.shared[context.ParentPath.String()] = make(modeGroups)
}
group := mtv.shared[context.ParentPath.String()].getOrCreate(modeName)
fieldName := context.Member.Name
if rules, ok := group.members[fieldName]; ok {
if rules.member != context.Member {
return Validations{}, fmt.Errorf("internal error: member mismatch for field %q", fieldName)
}
} else {
group.members[fieldName] = &fieldModeRules{
member: context.Member,
}
}
payloadValidations, err := mtv.validator.ExtractTagValidations(context, *tag.ValueTag)
if err != nil {
return Validations{}, err
}
group.members[fieldName].rules = append(group.members[fieldName].rules, modeRule{
value: value,
validations: payloadValidations,
})
return Validations{}, nil
}
func (mtv *memberTagValidator) Docs() TagDoc {
return TagDoc{
Tag: mtv.TagName(),
StabilityLevel: Alpha,
Scopes: mtv.ValidScopes().UnsortedList(),
Description: "Indicates that this field's validation depends on a discriminator.",
Args: []TagArgDoc{{
Name: "", // positional
Description: "<string>",
Docs: "the value of the discriminator for which this validation applies",
Type: codetags.ArgTypeString,
}, {
Name: "discriminator",
Description: "<string>",
Docs: "the name of the discriminator group",
Type: codetags.ArgTypeString,
}, {
Name: "value",
Description: "<string>",
Docs: "the value of the discriminator for which this validation applies",
Type: codetags.ArgTypeString,
}},
PayloadsType: codetags.ValueTypeTag,
PayloadsRequired: true,
}
}
type modeTypeOrFieldValidator struct {
shared map[string]modeGroups
}
func (modeTypeOrFieldValidator) Init(_ Config) {}
func (modeTypeOrFieldValidator) Name() string {
return "modeTypeOrFieldValidator"
}
func (mtfv *modeTypeOrFieldValidator) GetValidations(context Context) (Validations, error) {
// Extract the most concrete type possible.
if k := util.NonPointer(util.NativeType(context.Type)).Kind; k != types.Struct {
return Validations{}, nil
}
groups, ok := mtfv.shared[context.Path.String()]
if !ok || len(groups) == 0 {
return Validations{}, nil
}
var result Validations
// Sort group names for deterministic output
groupNames := make([]string, 0, len(groups))
for name := range groups {
groupNames = append(groupNames, name)
}
slices.Sort(groupNames)
for _, gn := range groupNames {
group := groups[gn]
if group.discriminatorMember == nil {
if len(group.members) > 0 {
if gn == "default" {
return Validations{}, fmt.Errorf("missing discriminator")
}
return Validations{}, fmt.Errorf("missing discriminator for group %q", gn)
}
continue
}
fieldNames := make([]string, 0, len(group.members))
for name := range group.members {
fieldNames = append(fieldNames, name)
}
slices.Sort(fieldNames)
for _, fn := range fieldNames {
rules := group.members[fn]
v, err := mtfv.generateModeFieldValidation(context, group, rules)
if err != nil {
return Validations{}, err
}
result.Add(v)
}
}
return result, nil
}
func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Context, group *modeGroup, rules *fieldModeRules) (Validations, error) {
fieldType := rules.member.Type
// Use the nilable form to handle missing values.
nilableFieldType := fieldType
fieldExprPrefix := ""
if !util.IsNilableType(nilableFieldType) {
nilableFieldType = types.PointerTo(nilableFieldType)
fieldExprPrefix = "&"
}
// Get the JSON name of the field
jsonName := rules.member.Name
if jt, ok := tags.LookupJSON(*rules.member); ok {
jsonName = jt.Name
}
// Default validation is Forbidden
defaultForbidden, err := mtfv.getForbiddenValidation(fieldType)
if err != nil {
return Validations{}, err
}
// Prepare ModalRules
// Aggregate rules by value
rulesByValue := make(map[string]Validations)
var values []string
for _, rule := range rules.rules {
if _, ok := rulesByValue[rule.value]; !ok {
values = append(values, rule.value)
}
v := rulesByValue[rule.value]
v.Add(rule.validations)
rulesByValue[rule.value] = v
}
slices.Sort(values)
var modalRules []any
for _, val := range values {
ruleValidations := rulesByValue[val]
wrapper := MultiWrapperFunction{
Functions: ruleValidations.Functions,
ObjType: nilableFieldType,
}
modalRules = append(modalRules, StructLiteral{
Type: types.Name{Package: libValidationPkg, Name: "ModalRule"},
TypeArgs: []*types.Type{nilableFieldType},
Fields: []StructLiteralField{
{Name: "Value", Value: val},
{Name: "Validation", Value: wrapper},
},
})
}
modalValidator := types.Name{Package: libValidationPkg, Name: "Modal"}
rulesSlice := SliceLiteral{
ElementType: types.Name{Package: libValidationPkg, Name: "ModalRule"},
ElementTypeArgs: []*types.Type{nilableFieldType},
Elements: modalRules,
}
// getValue extractor
getValue := FunctionLiteral{
Parameters: []ParamResult{{Name: "obj", Type: types.PointerTo(context.Type)}},
Results: []ParamResult{{Type: nilableFieldType}},
Body: fmt.Sprintf("return %sobj.%s", fieldExprPrefix, rules.member.Name),
}
// getDiscriminator extractor
discriminatorType := group.discriminatorMember.Type
getDiscriminator := FunctionLiteral{
Parameters: []ParamResult{{Name: "obj", Type: types.PointerTo(context.Type)}},
Results: []ParamResult{{Type: discriminatorType}},
Body: fmt.Sprintf("return obj.%s", group.discriminatorMember.Name),
}
// Calculate equivArg for ratcheting the value
var equivArg any
if util.IsDirectComparable(util.NonPointer(util.NativeType(fieldType))) {
equivArg = Identifier(validateDirectEqualPtr)
} else {
equivArg = Identifier(validateSemanticDeepEqual)
}
fn := Function(discriminatorTagName, DefaultFlags, modalValidator,
Literal(fmt.Sprintf("%q", jsonName)),
getValue,
getDiscriminator,
equivArg,
defaultForbidden,
rulesSlice,
)
return Validations{Functions: []FunctionGen{fn}}, nil
}
func (mtfv *modeTypeOrFieldValidator) getForbiddenValidation(t *types.Type) (any, error) {
var forbidden types.Name
nt := util.NativeType(t)
switch nt.Kind {
case types.Slice:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenSlice"}
case types.Map:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenMap"}
case types.Pointer:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenPointer"}
case types.Struct:
return nil, fmt.Errorf("can only be used on pointers to structs")
default:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenValue"}
}
fg := Function(forbiddenTagName, DefaultFlags, forbidden)
// Use the nilable form to match standard validation function signatures.
wrapperObjType := t
if !util.IsNilableType(t) {
wrapperObjType = types.PointerTo(t)
}
return MultiWrapperFunction{
Functions: []FunctionGen{fg},
ObjType: wrapperObjType,
}, nil
}