feedback: Address review comments

- Rename type params to Tstruct/Tfield/Tdisc for Subfield consistency
- Make DiscriminatedRule.Value generic over Tdisc and emit typed literals
- Move discriminator assignment adjacent to value assignment
- Fix mockEqual to test value equivalence and add pointer test
- Add test for value changed with discriminator unchanged
- Add regex validation for discriminator group names
- Disallow "default" as an explicit discriminator group name
- Add directComparable cross-reference comment for ratcheting
- Rename mode/modal terminology to discriminator/member
This commit is contained in:
Aaron Prindle
2026-02-13 17:27:11 +00:00
parent 98c52ccfd7
commit 70b90d9898
7 changed files with 438 additions and 371 deletions

View File

@@ -18,47 +18,44 @@ 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]
// DiscriminatedRule defines a validation to apply for a specific discriminator value.
type DiscriminatedRule[Tfield any, Tdisc comparable] struct {
Value Tdisc
Validation ValidateFunc[Tfield]
}
// Modal validates a value based on a discriminator value.
// Discriminated validates a member field 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],
func Discriminated[Tfield any, Tdisc comparable, Tstruct any](ctx context.Context, op operation.Operation, structPath *field.Path,
obj, oldObj *Tstruct, fieldName string, getMemberValue func(*Tstruct) Tfield, getDiscriminator func(*Tstruct) Tdisc,
equiv MatchFunc[Tfield], defaultValidation ValidateFunc[Tfield], rules []DiscriminatedRule[Tfield, Tdisc],
) field.ErrorList {
value := getMemberValue(obj)
var oldValue T
var oldDiscriminator D
discriminator := getDiscriminator(obj)
var oldValue Tfield
var oldDiscriminator Tdisc
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.Value == discriminator {
if rule.Validation == nil {
return nil
}
@@ -71,5 +68,4 @@ func Modal[T any, D comparable, P any](ctx context.Context, op operation.Operati
}
return nil
}

View File

@@ -0,0 +1,265 @@
/*
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 TestDiscriminated(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 compares pointer values by dereferencing, not by pointer identity.
mockEqual := func(a, b *string) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
testCases := []struct {
name string
opType operation.Type
discriminator string
oldDiscriminator string
value *string
oldValue *string
rules []DiscriminatedRule[*string, string]
defaultValidation ValidateFunc[*string]
expected field.ErrorList
}{
{
name: "matches rule, returns valid",
opType: operation.Create,
discriminator: "A",
oldDiscriminator: "A",
rules: []DiscriminatedRule[*string, 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: []DiscriminatedRule[*string, 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: []DiscriminatedRule[*string, string]{
{Value: "B", Validation: mockErrorMatch}, // would fail if run
},
defaultValidation: mockErrorDefault,
expected: nil,
},
{
name: "ratcheting: update, same value different pointers, skips validation",
opType: operation.Update,
discriminator: "B",
oldDiscriminator: "B",
value: strPtr("same"),
oldValue: strPtr("same"), // different pointer, same value
rules: []DiscriminatedRule[*string, 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: []DiscriminatedRule[*string, string]{
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
{
name: "ratcheting: update, value changed, discriminator unchanged, runs validation",
opType: operation.Update,
discriminator: "B",
oldDiscriminator: "B", // unchanged
value: strPtr("new"),
oldValue: strPtr("old"), // changed
rules: []DiscriminatedRule[*string, string]{
{Value: "B", Validation: mockErrorMatch},
},
defaultValidation: mockErrorDefault,
expected: errMatch,
},
{
name: "matches rule with nil validation, returns valid",
opType: operation.Create,
discriminator: "A",
rules: []DiscriminatedRule[*string, string]{
{Value: "A", Validation: nil},
},
defaultValidation: mockErrorDefault,
expected: nil,
},
{
name: "no match, runs default",
opType: operation.Create,
discriminator: "C",
rules: []DiscriminatedRule[*string, 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: []DiscriminatedRule[*string, string]{
{Value: "A", Validation: mockValid},
},
defaultValidation: nil,
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
oldDisc := tc.oldDiscriminator
if oldDisc == "" {
oldDisc = tc.discriminator
}
type StringP struct {
Val *string
Disc string
}
newObj := &StringP{Val: tc.value, Disc: tc.discriminator}
var oldObj *StringP
if tc.opType == operation.Update {
oldObj = &StringP{Val: tc.oldValue, Disc: oldDisc}
}
getVal := func(p *StringP) *string { return p.Val }
getDisc := func(p *StringP) string { return p.Disc }
got := Discriminated[*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)
}
})
}
}
func TestDiscriminatedIntDiscriminator(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")}
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
}
type IntP struct {
Val *string
Disc int
}
newObj := &IntP{Val: nil, Disc: 1}
getVal := func(p *IntP) *string { return p.Val }
getDisc := func(p *IntP) int { return p.Disc }
got := Discriminated[*string, int, IntP](context.Background(), operation.Operation{Type: operation.Create}, field.NewPath("root"), newObj, nil, "field", getVal, getDisc, mockEqual, mockErrorDefault, []DiscriminatedRule[*string, int]{
{Value: 1, Validation: mockErrorMatch},
})
if !reflect.DeepEqual(got, errMatch) {
t.Errorf("int discriminator: got %v want %v", got, errMatch)
}
}
func TestDiscriminatedBoolDiscriminator(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")}
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
}
type BoolP struct {
Val *string
Disc bool
}
newObj := &BoolP{Val: nil, Disc: true}
getVal := func(p *BoolP) *string { return p.Val }
getDisc := func(p *BoolP) bool { return p.Disc }
got := Discriminated[*string, bool, BoolP](context.Background(), operation.Operation{Type: operation.Create}, field.NewPath("root"), newObj, nil, "field", getVal, getDisc, mockEqual, mockErrorDefault, []DiscriminatedRule[*string, bool]{
{Value: true, Validation: mockErrorMatch},
})
if !reflect.DeepEqual(got, errMatch) {
t.Errorf("bool discriminator: got %v want %v", got, errMatch)
}
}
// strPtr returns a new pointer to a copy of s, guaranteeing a distinct allocation.
func strPtr(s string) *string {
return &s
}

View File

@@ -1,224 +0,0 @@
/*
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

@@ -18,7 +18,7 @@ limitations under the License.
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package.
package mode
package discriminator
import "k8s.io/code-generator/cmd/validation-gen/testscheme"

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package mode
package discriminator
import (
"testing"

View File

@@ -19,7 +19,7 @@ limitations under the License.
// Code generated by validation-gen. DO NOT EDIT.
package mode
package discriminator
import (
context "context"
@@ -99,14 +99,13 @@ func RegisterValidations(scheme *testscheme.Scheme) error {
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -118,8 +117,7 @@ func Validate_ChainedValidation(ctx context.Context, op operation.Operation, fld
}
errs = append(errs, validate.MaxLength(ctx, op, fldPath, obj, oldObj, 5)...)
return errs
},
},
}},
})...)
// field ChainedValidation.TypeMeta has no validation
@@ -131,14 +129,13 @@ func Validate_ChainedValidation(ctx context.Context, op operation.Operation, fld
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[[]string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj []string) field.ErrorList {
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 {
@@ -148,17 +145,15 @@ func Validate_Collections(ctx context.Context, op operation.Operation, fldPath *
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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[map[string]string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj map[string]string) field.ErrorList {
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 {
@@ -168,8 +163,7 @@ func Validate_Collections(ctx context.Context, op operation.Operation, fldPath *
return errs
}
return errs
},
},
}},
})...)
// field Collections.TypeMeta has no validation
@@ -182,14 +176,13 @@ func Validate_Collections(ctx context.Context, op operation.Operation, fldPath *
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -199,8 +192,7 @@ func Validate_ImplicitForbidden(ctx context.Context, op operation.Operation, fld
return errs
}
return errs
},
},
}},
})...)
// field ImplicitForbidden.TypeMeta has no validation
@@ -212,14 +204,13 @@ func Validate_ImplicitForbidden(ctx context.Context, op operation.Operation, fld
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -230,17 +221,15 @@ func Validate_MultipleDiscriminators(ctx context.Context, op operation.Operation
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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "B",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -251,8 +240,7 @@ func Validate_MultipleDiscriminators(ctx context.Context, op operation.Operation
return errs
}
return errs
},
},
}},
})...)
// field MultipleDiscriminators.TypeMeta has no validation
@@ -266,14 +254,13 @@ func Validate_MultipleDiscriminators(ctx context.Context, op operation.Operation
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, bool]{
{
Value: "true",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -284,17 +271,15 @@ func Validate_NonStringDiscriminator(ctx context.Context, op operation.Operation
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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, int]{
{
Value: "1",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -305,8 +290,7 @@ func Validate_NonStringDiscriminator(ctx context.Context, op operation.Operation
return errs
}
return errs
},
},
}},
})...)
// field NonStringDiscriminator.TypeMeta has no validation
@@ -320,14 +304,13 @@ func Validate_NonStringDiscriminator(ctx context.Context, op operation.Operation
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -337,11 +320,9 @@ func Validate_SharedField(ctx context.Context, op operation.Operation, fldPath *
return errs
}
return errs
},
},
}},
{
Value: "B",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -351,8 +332,7 @@ func Validate_SharedField(ctx context.Context, op operation.Operation, fldPath *
return errs
}
return errs
},
},
}},
})...)
// field SharedField.TypeMeta has no validation
@@ -364,14 +344,13 @@ func Validate_SharedField(ctx context.Context, op operation.Operation, fldPath *
// 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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "A",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -382,17 +361,15 @@ func Validate_StrictUnion(ctx context.Context, op operation.Operation, fldPath *
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 = append(errs, validate.Discriminated(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]{
}, []validate.DiscriminatedRule[*string, string]{
{
Value: "B",
Validation: func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
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 {
@@ -403,8 +380,7 @@ func Validate_StrictUnion(ctx context.Context, op operation.Operation, fldPath *
return errs
}
return errs
},
},
}},
})...)
// field StrictUnion.TypeMeta has no validation

View File

@@ -18,7 +18,9 @@ package validators
import (
"fmt"
"regexp"
"slices"
"strconv"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/code-generator/cmd/validation-gen/util"
@@ -32,66 +34,70 @@ const (
memberTagName = "k8s:member"
)
// validGroupNameRegex restricts discriminator group names to identifiers that
// start with a letter and contain only alphanumeric characters and underscores.
var validGroupNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]*$`)
func init() {
RegisterTagValidator(&modeTagValidator{modeDefinitions})
RegisterTagValidator(&memberTagValidator{modeDefinitions, nil})
RegisterTypeValidator(&modeTypeOrFieldValidator{modeDefinitions})
RegisterFieldValidator(&modeTypeOrFieldValidator{modeDefinitions})
RegisterTagValidator(&discriminatorTagValidator{discriminatorDefinitions})
RegisterTagValidator(&memberTagValidator{discriminatorDefinitions, nil})
RegisterTypeValidator(&discriminatorFieldValidator{discriminatorDefinitions})
RegisterFieldValidator(&discriminatorFieldValidator{discriminatorDefinitions})
}
// modeDefinitions stores all mode definitions found by tag validators.
// discriminatorDefinitions stores all discriminator definitions found by tag validators.
// Key is the struct path.
var modeDefinitions = map[string]modeGroups{}
var discriminatorDefinitions = map[string]discriminatorGroups{}
type modeGroups map[string]*modeGroup
type discriminatorGroups map[string]*discriminatorGroup
type modeGroup struct {
type discriminatorGroup struct {
name string
discriminatorMember *types.Member
// members maps field names to their rules in this mode group.
members map[string]*fieldModeRules
// members maps field names to their rules in this discriminator group.
members map[string]*fieldMemberRules
}
type fieldModeRules struct {
type fieldMemberRules struct {
member *types.Member
rules []modeRule
rules []memberRule
}
type modeRule struct {
type memberRule struct {
value string
validations Validations
}
func (mg modeGroups) getOrCreate(name string) *modeGroup {
func (mg discriminatorGroups) getOrCreate(name string) *discriminatorGroup {
if name == "" {
name = "default"
}
g, ok := mg[name]
if !ok {
g = &modeGroup{
g = &discriminatorGroup{
name: name,
members: make(map[string]*fieldModeRules),
members: make(map[string]*fieldMemberRules),
}
mg[name] = g
}
return g
}
type modeTagValidator struct {
shared map[string]modeGroups
type discriminatorTagValidator struct {
shared map[string]discriminatorGroups
}
func (mtv *modeTagValidator) Init(_ Config) {}
func (mtv *discriminatorTagValidator) Init(_ Config) {}
func (mtv *modeTagValidator) TagName() string {
func (mtv *discriminatorTagValidator) TagName() string {
return discriminatorTagName
}
func (mtv *modeTagValidator) ValidScopes() sets.Set[Scope] {
func (mtv *discriminatorTagValidator) ValidScopes() sets.Set[Scope] {
return sets.New(ScopeField)
}
func (mtv *modeTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
func (mtv *discriminatorTagValidator) 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")
}
@@ -101,22 +107,28 @@ func (mtv *modeTagValidator) GetValidations(context Context, tag codetags.Tag) (
}
if mtv.shared[context.ParentPath.String()] == nil {
mtv.shared[context.ParentPath.String()] = make(modeGroups)
mtv.shared[context.ParentPath.String()] = make(discriminatorGroups)
}
modeName := ""
groupName := ""
if nameArg, ok := tag.NamedArg("name"); ok {
modeName = nameArg.Value
groupName = nameArg.Value
}
group := mtv.shared[context.ParentPath.String()].getOrCreate(modeName)
if groupName != "" && !validGroupNameRegex.MatchString(groupName) {
return Validations{}, fmt.Errorf("discriminator group name must match %s, got %q", validGroupNameRegex.String(), groupName)
}
if groupName == "default" {
return Validations{}, fmt.Errorf("discriminator group name %q is reserved", groupName)
}
group := mtv.shared[context.ParentPath.String()].getOrCreate(groupName)
if group.discriminatorMember != nil && group.discriminatorMember != context.Member {
return Validations{}, fmt.Errorf("duplicate discriminator: %q", modeName)
return Validations{}, fmt.Errorf("duplicate discriminator: %q", groupName)
}
group.discriminatorMember = context.Member
return Validations{}, nil
}
func (mtv *modeTagValidator) Docs() TagDoc {
func (mtv *discriminatorTagValidator) Docs() TagDoc {
return TagDoc{
Tag: mtv.TagName(),
StabilityLevel: TagStabilityLevelAlpha,
@@ -132,7 +144,7 @@ func (mtv *modeTagValidator) Docs() TagDoc {
}
type memberTagValidator struct {
shared map[string]modeGroups
shared map[string]discriminatorGroups
validator TagValidationExtractor
}
@@ -167,9 +179,12 @@ func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag)
return Validations{}, fmt.Errorf("unsupported payload tag: %q", tag.ValueTag.Name)
}
modeName := ""
groupName := ""
if modeArg, ok := tag.NamedArg("discriminator"); ok {
modeName = modeArg.Value
groupName = modeArg.Value
}
if groupName == "default" {
return Validations{}, fmt.Errorf("discriminator group name %q is reserved", groupName)
}
value := ""
@@ -183,9 +198,9 @@ func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag)
}
if mtv.shared[context.ParentPath.String()] == nil {
mtv.shared[context.ParentPath.String()] = make(modeGroups)
mtv.shared[context.ParentPath.String()] = make(discriminatorGroups)
}
group := mtv.shared[context.ParentPath.String()].getOrCreate(modeName)
group := mtv.shared[context.ParentPath.String()].getOrCreate(groupName)
fieldName := context.Member.Name
if rules, ok := group.members[fieldName]; ok {
@@ -193,7 +208,7 @@ func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag)
return Validations{}, fmt.Errorf("internal error: member mismatch for field %q", fieldName)
}
} else {
group.members[fieldName] = &fieldModeRules{
group.members[fieldName] = &fieldMemberRules{
member: context.Member,
}
}
@@ -203,7 +218,7 @@ func (mtv *memberTagValidator) GetValidations(context Context, tag codetags.Tag)
return Validations{}, err
}
group.members[fieldName].rules = append(group.members[fieldName].rules, modeRule{
group.members[fieldName].rules = append(group.members[fieldName].rules, memberRule{
value: value,
validations: payloadValidations,
})
@@ -238,17 +253,17 @@ func (mtv *memberTagValidator) Docs() TagDoc {
}
}
type modeTypeOrFieldValidator struct {
shared map[string]modeGroups
type discriminatorFieldValidator struct {
shared map[string]discriminatorGroups
}
func (modeTypeOrFieldValidator) Init(_ Config) {}
func (discriminatorFieldValidator) Init(_ Config) {}
func (modeTypeOrFieldValidator) Name() string {
return "modeTypeOrFieldValidator"
func (discriminatorFieldValidator) Name() string {
return "discriminatorFieldValidator"
}
func (mtfv *modeTypeOrFieldValidator) GetValidations(context Context) (Validations, error) {
func (mtfv *discriminatorFieldValidator) 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
@@ -288,7 +303,7 @@ func (mtfv *modeTypeOrFieldValidator) GetValidations(context Context) (Validatio
for _, fn := range fieldNames {
rules := group.members[fn]
v, err := mtfv.generateModeFieldValidation(context, group, rules)
v, err := mtfv.generateMemberFieldValidation(context, group, rules)
if err != nil {
return Validations{}, err
}
@@ -299,7 +314,7 @@ func (mtfv *modeTypeOrFieldValidator) GetValidations(context Context) (Validatio
return result, nil
}
func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Context, group *modeGroup, rules *fieldModeRules) (Validations, error) {
func (mtfv *discriminatorFieldValidator) generateMemberFieldValidation(context Context, group *discriminatorGroup, rules *fieldMemberRules) (Validations, error) {
fieldType := rules.member.Type
// Use the nilable form to handle missing values.
@@ -322,7 +337,7 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
return Validations{}, err
}
// Prepare ModalRules
// Prepare DiscriminatedRules
// Aggregate rules by value
rulesByValue := make(map[string]Validations)
var values []string
@@ -336,7 +351,9 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
}
slices.Sort(values)
var modalRules []any
discriminatorType := group.discriminatorMember.Type
var discriminatedRules []any
for _, val := range values {
ruleValidations := rulesByValue[val]
@@ -345,22 +362,29 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
ObjType: nilableFieldType,
}
modalRules = append(modalRules, StructLiteral{
Type: types.Name{Package: libValidationPkg, Name: "ModalRule"},
TypeArgs: []*types.Type{nilableFieldType},
// Convert the string tag value to the appropriate typed Go literal
// for the discriminator type.
typedValue, err := convertDiscriminatorValue(val, discriminatorType)
if err != nil {
return Validations{}, fmt.Errorf("invalid discriminator value %q: %w", val, err)
}
discriminatedRules = append(discriminatedRules, StructLiteral{
Type: types.Name{Package: libValidationPkg, Name: "DiscriminatedRule"},
TypeArgs: []*types.Type{nilableFieldType, discriminatorType},
Fields: []StructLiteralField{
{Name: "Value", Value: val},
{Name: "Value", Value: typedValue},
{Name: "Validation", Value: wrapper},
},
})
}
modalValidator := types.Name{Package: libValidationPkg, Name: "Modal"}
discriminatedValidator := types.Name{Package: libValidationPkg, Name: "Discriminated"}
rulesSlice := SliceLiteral{
ElementType: types.Name{Package: libValidationPkg, Name: "ModalRule"},
ElementTypeArgs: []*types.Type{nilableFieldType},
Elements: modalRules,
ElementType: types.Name{Package: libValidationPkg, Name: "DiscriminatedRule"},
ElementTypeArgs: []*types.Type{nilableFieldType, discriminatorType},
Elements: discriminatedRules,
}
// getValue extractor
@@ -371,14 +395,15 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
}
// 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
// directComparable is used to determine whether we can use the direct
// comparison operator "==" or need to use the semantic DeepEqual when
// looking up and comparing correlated list elements for validation ratcheting.
var equivArg any
if util.IsDirectComparable(util.NonPointer(util.NativeType(fieldType))) {
equivArg = Identifier(validateDirectEqualPtr)
@@ -386,7 +411,7 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
equivArg = Identifier(validateSemanticDeepEqual)
}
fn := Function(discriminatorTagName, DefaultFlags, modalValidator,
fn := Function(discriminatorTagName, DefaultFlags, discriminatedValidator,
Literal(fmt.Sprintf("%q", jsonName)),
getValue,
getDiscriminator,
@@ -398,7 +423,7 @@ func (mtfv *modeTypeOrFieldValidator) generateModeFieldValidation(context Contex
return Validations{Functions: []FunctionGen{fn}}, nil
}
func (mtfv *modeTypeOrFieldValidator) getForbiddenValidation(t *types.Type) (any, error) {
func (mtfv *discriminatorFieldValidator) getForbiddenValidation(t *types.Type) (any, error) {
var forbidden types.Name
nt := util.NativeType(t)
switch nt.Kind {
@@ -409,7 +434,7 @@ func (mtfv *modeTypeOrFieldValidator) getForbiddenValidation(t *types.Type) (any
case types.Pointer:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenPointer"}
case types.Struct:
return nil, fmt.Errorf("modal member fields of struct type must be pointers")
return nil, fmt.Errorf("discriminated member fields of struct type must be pointers")
default:
forbidden = types.Name{Package: libValidationPkg, Name: "ForbiddenValue"}
}
@@ -427,3 +452,32 @@ func (mtfv *modeTypeOrFieldValidator) getForbiddenValidation(t *types.Type) (any
ObjType: wrapperObjType,
}, nil
}
// convertDiscriminatorValue converts a string tag value to the appropriate
// typed Go literal for the given discriminator type.
func convertDiscriminatorValue(val string, discType *types.Type) (any, error) {
nt := util.NonPointer(util.NativeType(discType))
if nt.Kind != types.Builtin {
return nil, fmt.Errorf("unsupported discriminator type: %s", nt.Name.Name)
}
switch nt.Name.Name {
case "string":
return val, nil
case "bool":
b, err := strconv.ParseBool(val)
if err != nil {
return nil, fmt.Errorf("cannot parse %q as bool: %w", val, err)
}
return b, nil
default:
if types.IsInteger(nt) {
i, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, fmt.Errorf("cannot parse %q as integer: %w", val, err)
}
return int(i), nil
}
return nil, fmt.Errorf("unsupported discriminator type: %s", nt.Name.Name)
}
}