mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-28 01:34:19 +00:00
Add enumExclude support to enum validator
This commit is contained in:
@@ -25,16 +25,60 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Enum verifies that the specified value is one of the valid symbols.
|
||||
// This is for string enums only.
|
||||
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, symbols sets.Set[T]) field.ErrorList {
|
||||
// Enum verifies that a given value is a member of a set of enum values.
|
||||
// Exclude Rules that apply when options are enabled or disabled are also considered.
|
||||
// If ANY exclude rule matches for a value, that value is excluded from the enum when validating.
|
||||
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, values sets.Set[T], exclusions []EnumExclusion[T]) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if !symbols.Has(*value) {
|
||||
symbolList := symbols.UnsortedList()
|
||||
slices.Sort(symbolList)
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, symbolList)}
|
||||
if !values.Has(*value) {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, values, exclusions))}
|
||||
}
|
||||
for _, rule := range exclusions {
|
||||
if rule.ExcludeWhenEnabled == op.HasOption(rule.Option) && *value == rule.Value {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, values, exclusions))}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// supportedValues returns a sorted list of supported values.
|
||||
// Excluded enum values are not included in the list.
|
||||
func supportedValues[T ~string](op operation.Operation, values sets.Set[T], exclusions []EnumExclusion[T]) []T {
|
||||
res := make([]T, 0, len(values))
|
||||
for key := range values {
|
||||
if isExcluded(op, exclusions, key) {
|
||||
continue
|
||||
}
|
||||
res = append(res, key)
|
||||
}
|
||||
slices.Sort(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// NewExclusions prepares a set of enum exclusion rules for validation.
|
||||
func NewExclusions[T ~string](rules ...EnumExclusion[T]) []EnumExclusion[T] {
|
||||
return rules
|
||||
}
|
||||
|
||||
// EnumExclusion represents a single enum exclusion rule.
|
||||
type EnumExclusion[T ~string] struct {
|
||||
// Value specifies the enum value to be conditionally excluded.
|
||||
Value T
|
||||
// ExcludeWhenEnabled determines the condition for exclusion.
|
||||
// If true, the value is excluded if the option is present.
|
||||
// If false, the value is excluded if the option is NOT present.
|
||||
ExcludeWhenEnabled bool
|
||||
// Option is the name of the feature option that controls the exclusion.
|
||||
Option string
|
||||
}
|
||||
|
||||
func isExcluded[T ~string](op operation.Operation, exclusions []EnumExclusion[T], value T) bool {
|
||||
for _, rule := range exclusions {
|
||||
if rule.Value == value && rule.ExcludeWhenEnabled == op.HasOption(rule.Option) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -27,38 +27,43 @@ import (
|
||||
|
||||
func TestEnum(t *testing.T) {
|
||||
cases := []struct {
|
||||
value string
|
||||
valid sets.Set[string]
|
||||
err bool
|
||||
name string
|
||||
value string
|
||||
valid sets.Set[string]
|
||||
expectErr string
|
||||
}{{
|
||||
value: "a",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
err: false,
|
||||
name: "valid value",
|
||||
value: "a",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
expectErr: "",
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New("c", "a", "b"),
|
||||
err: true,
|
||||
name: "invalid value",
|
||||
value: "x",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
expectErr: `fldpath: Unsupported value: "x": supported values: "a", "b", "c"`,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fldpath"), &tc.value, nil, tc.valid, nil)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
if want, got := `supported values: "a", "b", "c"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,37 +76,149 @@ func TestEnumTypedef(t *testing.T) {
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
value StringType
|
||||
valid sets.Set[StringType]
|
||||
err bool
|
||||
name string
|
||||
value StringType
|
||||
valid sets.Set[StringType]
|
||||
expectErr string
|
||||
}{{
|
||||
value: "foo",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: false,
|
||||
name: "valid value",
|
||||
value: "foo",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
expectErr: "",
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: true,
|
||||
name: "invalid value",
|
||||
value: "x",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
expectErr: `fldpath: Unsupported value: "x": supported values: "bar", "foo", "qux"`,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fldpath"), &tc.value, nil, tc.valid, nil)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
if want, got := `supported values: "bar", "foo", "qux"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumExclude(t *testing.T) {
|
||||
type TestEnum string
|
||||
const (
|
||||
ValueA TestEnum = "A"
|
||||
ValueB TestEnum = "B"
|
||||
ValueC TestEnum = "C"
|
||||
ValueD TestEnum = "D"
|
||||
)
|
||||
|
||||
const (
|
||||
FeatureA = "FeatureA"
|
||||
FeatureB = "FeatureB"
|
||||
)
|
||||
|
||||
testEnumValues := sets.New(ValueA, ValueB, ValueC, ValueD)
|
||||
testEnumExclusions := []EnumExclusion[TestEnum]{
|
||||
{Value: ValueA, Option: FeatureA, ExcludeWhen: true},
|
||||
{Value: ValueB, Option: FeatureB, ExcludeWhen: false},
|
||||
{Value: ValueD, Option: FeatureA, ExcludeWhen: true},
|
||||
{Value: ValueD, Option: FeatureB, ExcludeWhen: false},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
value TestEnum
|
||||
opts []string
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "no options, A is valid",
|
||||
value: ValueA,
|
||||
},
|
||||
{
|
||||
name: "no options, B is invalid",
|
||||
value: ValueB,
|
||||
expectErr: `fld: Unsupported value: "B": supported values: "A", "C"`,
|
||||
},
|
||||
{
|
||||
name: "no options, D is invalid",
|
||||
value: ValueD,
|
||||
expectErr: `fld: Unsupported value: "D": supported values: "A", "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA enabled, A is invalid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureA},
|
||||
expectErr: `fld: Unsupported value: "A": supported values: "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA enabled, B is invalid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureA},
|
||||
expectErr: `fld: Unsupported value: "B": supported values: "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureB enabled, A is valid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureB enabled, B is valid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, A is invalid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
expectErr: `fld: Unsupported value: "A": supported values: "B", "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, B is valid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, D is invalid",
|
||||
value: ValueD,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
expectErr: `fld: Unsupported value: "D": supported values: "B", "C"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create, Options: tc.opts}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fld"), &tc.value, nil, testEnumValues, testEnumExclusions)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user