mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-29 22:46:12 +00:00
Add validators: eachkey, eachval, subfield
Introduce a composable set of tags for validating child data. This allows for point-of-use validation of shared types. Co-authored-by: Tim Hockin <thockin@google.com> Co-authored-by: Aaron Prindle <aprindle@google.com> Co-authored-by: Yongrui Lin <yongrlin@google.com>
This commit is contained in:
parent
b5f9a00258
commit
31f4637217
119
staging/src/k8s.io/apimachinery/pkg/api/validate/each.go
Normal file
119
staging/src/k8s.io/apimachinery/pkg/api/validate/each.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/operation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompareFunc is a function that compares two values of the same type.
|
||||||
|
type CompareFunc[T any] func(T, T) bool
|
||||||
|
|
||||||
|
// EachSliceVal validates each element of newSlice with the specified
|
||||||
|
// validation function. The comparison function is used to find the
|
||||||
|
// corresponding value in oldSlice. The value-type of the slices is assumed to
|
||||||
|
// not be nilable.
|
||||||
|
func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T,
|
||||||
|
cmp CompareFunc[T], validator ValidateFunc[*T]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
for i, val := range newSlice {
|
||||||
|
var old *T
|
||||||
|
if cmp != nil && len(oldSlice) > 0 {
|
||||||
|
old = lookup(oldSlice, val, cmp)
|
||||||
|
}
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath.Index(i), &val, old)...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachSliceValNilable validates each element of newSlice with the specified
|
||||||
|
// validation function. The comparison function is used to find the
|
||||||
|
// corresponding value in oldSlice. The value-type of the slices is assumed to
|
||||||
|
// be nilable.
|
||||||
|
func EachSliceValNilable[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T,
|
||||||
|
cmp CompareFunc[T], validator ValidateFunc[T]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
for i, val := range newSlice {
|
||||||
|
var old T
|
||||||
|
if cmp != nil && len(oldSlice) > 0 {
|
||||||
|
p := lookup(oldSlice, val, cmp)
|
||||||
|
if p != nil {
|
||||||
|
old = *p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath.Index(i), val, old)...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup returns a pointer to the first element in the list that matches the
|
||||||
|
// target, according to the provided comparison function, or else nil.
|
||||||
|
func lookup[T any](list []T, target T, cmp func(T, T) bool) *T {
|
||||||
|
for i := range list {
|
||||||
|
if cmp(list[i], target) {
|
||||||
|
return &list[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachMapVal validates each element of newMap with the specified validation
|
||||||
|
// function and, if the corresponding key is found in oldMap, the old value.
|
||||||
|
// The value-type of the slices is assumed to not be nilable.
|
||||||
|
func EachMapVal[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V,
|
||||||
|
validator ValidateFunc[*V]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
for key, val := range newMap {
|
||||||
|
var old *V
|
||||||
|
if o, found := oldMap[key]; found {
|
||||||
|
old = &o
|
||||||
|
}
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath.Key(string(key)), &val, old)...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachMapValNilable validates each element of newMap with the specified
|
||||||
|
// validation function and, if the corresponding key is found in oldMap, the
|
||||||
|
// old value. The value-type of the slices is assumed to be nilable.
|
||||||
|
func EachMapValNilable[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V,
|
||||||
|
validator ValidateFunc[V]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
for key, val := range newMap {
|
||||||
|
var old V
|
||||||
|
if o, found := oldMap[key]; found {
|
||||||
|
old = o
|
||||||
|
}
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath.Key(string(key)), val, old)...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// EachMapKey validates each element of newMap with the specified
|
||||||
|
// validation function. The oldMap argument is not used.
|
||||||
|
func EachMapKey[K ~string, T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]T,
|
||||||
|
validator ValidateFunc[*K]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
for key := range newMap {
|
||||||
|
// Note: the field path is the field, not the key.
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath, &key, nil)...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
316
staging/src/k8s.io/apimachinery/pkg/api/validate/each_test.go
Normal file
316
staging/src/k8s.io/apimachinery/pkg/api/validate/each_test.go
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/operation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestStruct struct {
|
||||||
|
I int
|
||||||
|
D string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachSliceVal(t *testing.T) {
|
||||||
|
testEachSliceVal(t, "valid", []int{11, 12, 13})
|
||||||
|
testEachSliceVal(t, "valid", []string{"a", "b", "c"})
|
||||||
|
testEachSliceVal(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||||
|
|
||||||
|
testEachSliceVal(t, "empty", []int{})
|
||||||
|
testEachSliceVal(t, "empty", []string{})
|
||||||
|
testEachSliceVal(t, "empty", []TestStruct{})
|
||||||
|
|
||||||
|
testEachSliceVal[int](t, "nil", nil)
|
||||||
|
testEachSliceVal[string](t, "nil", nil)
|
||||||
|
testEachSliceVal[TestStruct](t, "nil", nil)
|
||||||
|
|
||||||
|
testEachSliceValUpdate(t, "valid", []int{11, 12, 13})
|
||||||
|
testEachSliceValUpdate(t, "valid", []string{"a", "b", "c"})
|
||||||
|
testEachSliceValUpdate(t, "valid", []TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||||
|
|
||||||
|
testEachSliceValUpdate(t, "empty", []int{})
|
||||||
|
testEachSliceValUpdate(t, "empty", []string{})
|
||||||
|
testEachSliceValUpdate(t, "empty", []TestStruct{})
|
||||||
|
|
||||||
|
testEachSliceValUpdate[int](t, "nil", nil)
|
||||||
|
testEachSliceValUpdate[string](t, "nil", nil)
|
||||||
|
testEachSliceValUpdate[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachSliceVal[T any](t *testing.T, name string, input []T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||||
|
if oldVal != nil {
|
||||||
|
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachSliceValUpdate[T any](t *testing.T, name string, input []T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||||
|
if oldVal == nil {
|
||||||
|
t.Fatalf("expected non-nil oldVal")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(*newVal, *oldVal) {
|
||||||
|
t.Errorf("expected oldVal == newVal, got %v, %v", *oldVal, *newVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
old := make([]T, len(input))
|
||||||
|
copy(old, input)
|
||||||
|
slices.Reverse(old)
|
||||||
|
cmp := func(a, b T) bool { return reflect.DeepEqual(a, b) }
|
||||||
|
_ = EachSliceVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, cmp, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachSliceValNilablePointer(t *testing.T) {
|
||||||
|
testEachSliceValNilable(t, "valid", []*int{ptr.To(11), ptr.To(12), ptr.To(13)})
|
||||||
|
testEachSliceValNilable(t, "valid", []*string{ptr.To("a"), ptr.To("b"), ptr.To("c")})
|
||||||
|
testEachSliceValNilable(t, "valid", []*TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||||
|
|
||||||
|
testEachSliceValNilable(t, "empty", []*int{})
|
||||||
|
testEachSliceValNilable(t, "empty", []*string{})
|
||||||
|
testEachSliceValNilable(t, "empty", []*TestStruct{})
|
||||||
|
|
||||||
|
testEachSliceValNilable[int](t, "nil", nil)
|
||||||
|
testEachSliceValNilable[string](t, "nil", nil)
|
||||||
|
testEachSliceValNilable[TestStruct](t, "nil", nil)
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", []*int{ptr.To(11), ptr.To(12), ptr.To(13)})
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", []*string{ptr.To("a"), ptr.To("b"), ptr.To("c")})
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", []*TestStruct{{11, "a"}, {12, "b"}, {13, "c"}})
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", []*int{})
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", []*string{})
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", []*TestStruct{})
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate[int](t, "nil", nil)
|
||||||
|
testEachSliceValNilableUpdate[string](t, "nil", nil)
|
||||||
|
testEachSliceValNilableUpdate[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachSliceValNilableSlice(t *testing.T) {
|
||||||
|
testEachSliceValNilable(t, "valid", [][]int{{11, 12, 13}, {12, 13, 14}, {13, 14, 15}})
|
||||||
|
testEachSliceValNilable(t, "valid", [][]string{{"a", "b", "c"}, {"b", "c", "d"}, {"c", "d", "e"}})
|
||||||
|
testEachSliceValNilable(t, "valid", [][]TestStruct{
|
||||||
|
{{11, "a"}, {12, "b"}, {13, "c"}},
|
||||||
|
{{12, "a"}, {13, "b"}, {14, "c"}},
|
||||||
|
{{13, "a"}, {14, "b"}, {15, "c"}}})
|
||||||
|
|
||||||
|
testEachSliceValNilable(t, "empty", [][]int{{}, {}, {}})
|
||||||
|
testEachSliceValNilable(t, "empty", [][]string{{}, {}, {}})
|
||||||
|
testEachSliceValNilable(t, "empty", [][]TestStruct{{}, {}, {}})
|
||||||
|
|
||||||
|
testEachSliceValNilable[int](t, "nil", nil)
|
||||||
|
testEachSliceValNilable[string](t, "nil", nil)
|
||||||
|
testEachSliceValNilable[TestStruct](t, "nil", nil)
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", [][]int{{11, 12, 13}, {12, 13, 14}, {13, 14, 15}})
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", [][]string{{"a", "b", "c"}, {"b", "c", "d"}, {"c", "d", "e"}})
|
||||||
|
testEachSliceValNilableUpdate(t, "valid", [][]TestStruct{
|
||||||
|
{{11, "a"}, {12, "b"}, {13, "c"}},
|
||||||
|
{{12, "a"}, {13, "b"}, {14, "c"}},
|
||||||
|
{{13, "a"}, {14, "b"}, {15, "c"}}})
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", [][]int{{}, {}, {}})
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", [][]string{{}, {}, {}})
|
||||||
|
testEachSliceValNilableUpdate(t, "empty", [][]TestStruct{{}, {}, {}})
|
||||||
|
|
||||||
|
testEachSliceValNilableUpdate[int](t, "nil", nil)
|
||||||
|
testEachSliceValNilableUpdate[string](t, "nil", nil)
|
||||||
|
testEachSliceValNilableUpdate[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachSliceValNilable[T any](t *testing.T, name string, input []T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal T) field.ErrorList {
|
||||||
|
if !reflect.DeepEqual(oldVal, zero) {
|
||||||
|
t.Errorf("expected nil oldVal, got %v", oldVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = EachSliceValNilable(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, nil, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachSliceValNilableUpdate[T any](t *testing.T, name string, input []T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal T) field.ErrorList {
|
||||||
|
if reflect.DeepEqual(oldVal, zero) {
|
||||||
|
t.Fatalf("expected non-nil oldVal")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(newVal, oldVal) {
|
||||||
|
t.Errorf("expected oldVal == newVal, got %v, %v", oldVal, newVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
old := make([]T, len(input))
|
||||||
|
copy(old, input)
|
||||||
|
slices.Reverse(old)
|
||||||
|
cmp := func(a, b T) bool { return reflect.DeepEqual(a, b) }
|
||||||
|
_ = EachSliceValNilable(context.Background(), operation.Operation{}, field.NewPath("test"), input, old, cmp, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachMapVal(t *testing.T) {
|
||||||
|
testEachMapVal(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
|
||||||
|
testEachMapVal(t, "valid", map[string]string{"A": "a", "B": "b", "C": "c"})
|
||||||
|
testEachMapVal(t, "valid", map[string]TestStruct{"one": {11, "a"}, "two": {12, "b"}, "three": {13, "c"}})
|
||||||
|
|
||||||
|
testEachMapVal(t, "empty", map[string]int{})
|
||||||
|
testEachMapVal(t, "empty", map[string]string{})
|
||||||
|
testEachMapVal(t, "empty", map[string]TestStruct{})
|
||||||
|
|
||||||
|
testEachMapVal[int](t, "nil", nil)
|
||||||
|
testEachMapVal[string](t, "nil", nil)
|
||||||
|
testEachMapVal[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachMapVal[T any](t *testing.T, name string, input map[string]T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *T) field.ErrorList {
|
||||||
|
if oldVal != nil {
|
||||||
|
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = EachMapVal(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachMapValNilablePointer(t *testing.T) {
|
||||||
|
testEachMapValNilable(t, "valid", map[string]*int{"one": ptr.To(11), "two": ptr.To(12), "three": ptr.To(13)})
|
||||||
|
testEachMapValNilable(t, "valid", map[string]*string{"A": ptr.To("a"), "B": ptr.To("b"), "C": ptr.To("c")})
|
||||||
|
testEachMapValNilable(t, "valid", map[string]*TestStruct{"one": {11, "a"}, "two": {12, "b"}, "three": {13, "c"}})
|
||||||
|
|
||||||
|
testEachMapValNilable(t, "empty", map[string]*int{})
|
||||||
|
testEachMapValNilable(t, "empty", map[string]*string{})
|
||||||
|
testEachMapValNilable(t, "empty", map[string]*TestStruct{})
|
||||||
|
|
||||||
|
testEachMapValNilable[int](t, "nil", nil)
|
||||||
|
testEachMapValNilable[string](t, "nil", nil)
|
||||||
|
testEachMapValNilable[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEachMapValNilableSlice(t *testing.T) {
|
||||||
|
testEachMapValNilable(t, "valid", map[string][]int{
|
||||||
|
"one": {11, 12, 13},
|
||||||
|
"two": {12, 13, 14},
|
||||||
|
"three": {13, 14, 15}})
|
||||||
|
testEachMapValNilable(t, "valid", map[string][]string{
|
||||||
|
"A": {"a", "b", "c"},
|
||||||
|
"B": {"b", "c", "d"},
|
||||||
|
"C": {"c", "d", "e"}})
|
||||||
|
testEachMapValNilable(t, "valid", map[string][]TestStruct{
|
||||||
|
"one": {{11, "a"}, {12, "b"}, {13, "c"}},
|
||||||
|
"two": {{12, "a"}, {13, "b"}, {14, "c"}},
|
||||||
|
"three": {{13, "a"}, {14, "b"}, {15, "c"}}})
|
||||||
|
|
||||||
|
testEachMapValNilable(t, "empty", map[string][]int{"a": {}, "b": {}, "c": {}})
|
||||||
|
testEachMapValNilable(t, "empty", map[string][]string{"a": {}, "b": {}, "c": {}})
|
||||||
|
testEachMapValNilable(t, "empty", map[string][]TestStruct{"a": {}, "b": {}, "c": {}})
|
||||||
|
|
||||||
|
testEachMapValNilable[int](t, "nil", nil)
|
||||||
|
testEachMapValNilable[string](t, "nil", nil)
|
||||||
|
testEachMapValNilable[TestStruct](t, "nil", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachMapValNilable[T any](t *testing.T, name string, input map[string]T) {
|
||||||
|
var zero T
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal T) field.ErrorList {
|
||||||
|
if !reflect.DeepEqual(oldVal, zero) {
|
||||||
|
t.Errorf("expected nil oldVal, got %v", oldVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = EachMapValNilable(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type StringType string
|
||||||
|
|
||||||
|
func TestEachMapKey(t *testing.T) {
|
||||||
|
testEachMapKey(t, "valid", map[string]int{"one": 11, "two": 12, "three": 13})
|
||||||
|
testEachMapKey(t, "valid", map[StringType]string{"A": "a", "B": "b", "C": "c"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEachMapKey[K ~string, V any](t *testing.T, name string, input map[K]V) {
|
||||||
|
var zero K
|
||||||
|
t.Run(fmt.Sprintf("%s(%T)", name, zero), func(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
vfn := func(ctx context.Context, op operation.Operation, fldPath *field.Path, newVal, oldVal *K) field.ErrorList {
|
||||||
|
if oldVal != nil {
|
||||||
|
t.Errorf("expected nil oldVal, got %v", *oldVal)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_ = EachMapKey(context.Background(), operation.Operation{}, field.NewPath("test"), input, nil, vfn)
|
||||||
|
if calls != len(input) {
|
||||||
|
t.Errorf("expected %d calls, got %d", len(input), calls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
41
staging/src/k8s.io/apimachinery/pkg/api/validate/subfield.go
Normal file
41
staging/src/k8s.io/apimachinery/pkg/api/validate/subfield.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/operation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetFieldFunc is a function that extracts a field from a type and returns a
|
||||||
|
// nilable value.
|
||||||
|
type GetFieldFunc[Tstruct any, Tfield any] func(*Tstruct) Tfield
|
||||||
|
|
||||||
|
// Subfield validates a subfield of a struct against a validator function.
|
||||||
|
func Subfield[Tstruct any, Tfield any](ctx context.Context, op operation.Operation, fldPath *field.Path, newStruct, oldStruct *Tstruct,
|
||||||
|
fldName string, getField GetFieldFunc[Tstruct, Tfield], validator ValidateFunc[Tfield]) field.ErrorList {
|
||||||
|
var errs field.ErrorList
|
||||||
|
newVal := getField(newStruct)
|
||||||
|
var oldVal Tfield
|
||||||
|
if oldStruct != nil {
|
||||||
|
oldVal = getField(oldStruct)
|
||||||
|
}
|
||||||
|
errs = append(errs, validator(ctx, op, fldPath.Child(fldName), newVal, oldVal)...)
|
||||||
|
return errs
|
||||||
|
}
|
@ -0,0 +1,430 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/gengo/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
listTypeTagName = "k8s:listType"
|
||||||
|
ListMapKeyTagName = "k8s:listMapKey"
|
||||||
|
eachValTagName = "k8s:eachVal"
|
||||||
|
eachKeyTagName = "k8s:eachKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
// We keep the eachVal and eachKey validators around because the main
|
||||||
|
// code-generation logic calls them directly. We could move them into the main
|
||||||
|
// pkg, but it's easier and cleaner to leave them here.
|
||||||
|
var globalEachVal *eachValTagValidator
|
||||||
|
var globalEachKey *eachKeyTagValidator
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Lists with list-map semantics are comprised of multiple tags, which need
|
||||||
|
// to share information between them.
|
||||||
|
shared := map[string]*listMap{} // keyed by the fieldpath
|
||||||
|
RegisterTagValidator(listTypeTagValidator{shared})
|
||||||
|
RegisterTagValidator(listMapKeyTagValidator{shared})
|
||||||
|
|
||||||
|
globalEachVal = &eachValTagValidator{shared, nil}
|
||||||
|
RegisterTagValidator(globalEachVal)
|
||||||
|
|
||||||
|
globalEachKey = &eachKeyTagValidator{nil}
|
||||||
|
RegisterTagValidator(globalEachKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This applies to all tags in this file.
|
||||||
|
var listTagsValidScopes = sets.New(ScopeAny)
|
||||||
|
|
||||||
|
// listMap collects information about a single list with map semantics.
|
||||||
|
type listMap struct {
|
||||||
|
declaredAsMap bool
|
||||||
|
keyFields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type listTypeTagValidator struct {
|
||||||
|
byFieldPath map[string]*listMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listTypeTagValidator) Init(Config) {}
|
||||||
|
|
||||||
|
func (listTypeTagValidator) TagName() string {
|
||||||
|
return listTypeTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listTypeTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return listTagsValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lttv listTypeTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
|
||||||
|
t := context.Type
|
||||||
|
if t.Kind == types.Alias {
|
||||||
|
t = t.Underlying
|
||||||
|
}
|
||||||
|
if t.Kind != types.Slice && t.Kind != types.Array {
|
||||||
|
return Validations{}, fmt.Errorf("can only be used on list types")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch payload {
|
||||||
|
case "atomic", "set":
|
||||||
|
// Allowed but no special handling.
|
||||||
|
case "map":
|
||||||
|
if realType(t.Elem).Kind != types.Struct {
|
||||||
|
return Validations{}, fmt.Errorf("only lists of structs can be list-maps")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the fact that this list is a map.
|
||||||
|
if lttv.byFieldPath[context.Path.String()] == nil {
|
||||||
|
lttv.byFieldPath[context.Path.String()] = &listMap{}
|
||||||
|
}
|
||||||
|
lm := lttv.byFieldPath[context.Path.String()]
|
||||||
|
lm.declaredAsMap = true
|
||||||
|
default:
|
||||||
|
return Validations{}, fmt.Errorf("unknown list type %q", payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This tag doesn't generate any validations. It just accumulates
|
||||||
|
// information for other tags to use.
|
||||||
|
return Validations{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func realType(t *types.Type) *types.Type {
|
||||||
|
for {
|
||||||
|
if t.Kind == types.Alias {
|
||||||
|
t = t.Underlying
|
||||||
|
} else if t.Kind == types.Pointer {
|
||||||
|
t = t.Elem
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lttv listTypeTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: lttv.TagName(),
|
||||||
|
Scopes: lttv.ValidScopes().UnsortedList(),
|
||||||
|
Description: "Declares a list field's semantic type.",
|
||||||
|
Payloads: []TagPayloadDoc{{
|
||||||
|
Description: "<type>",
|
||||||
|
Docs: "map | atomic",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
type listMapKeyTagValidator struct {
|
||||||
|
byFieldPath map[string]*listMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listMapKeyTagValidator) Init(Config) {}
|
||||||
|
|
||||||
|
func (listMapKeyTagValidator) TagName() string {
|
||||||
|
return ListMapKeyTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listMapKeyTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return listTagsValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lmktv listMapKeyTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
|
||||||
|
t := context.Type
|
||||||
|
if t.Kind == types.Alias {
|
||||||
|
t = t.Underlying
|
||||||
|
}
|
||||||
|
if t.Kind != types.Slice && t.Kind != types.Array {
|
||||||
|
return Validations{}, fmt.Errorf("can only be used on list types")
|
||||||
|
}
|
||||||
|
if realType(t.Elem).Kind != types.Struct {
|
||||||
|
return Validations{}, fmt.Errorf("only lists of structs can be list-maps")
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldName string
|
||||||
|
if memb := getMemberByJSON(realType(t.Elem), payload); memb == nil {
|
||||||
|
return Validations{}, fmt.Errorf("no field for JSON name %q", payload)
|
||||||
|
} else if k := realType(memb.Type).Kind; k != types.Builtin {
|
||||||
|
return Validations{}, fmt.Errorf("only primitive types can be list-map keys, not %s", k)
|
||||||
|
} else {
|
||||||
|
fieldName = memb.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if lmktv.byFieldPath[context.Path.String()] == nil {
|
||||||
|
lmktv.byFieldPath[context.Path.String()] = &listMap{}
|
||||||
|
}
|
||||||
|
lm := lmktv.byFieldPath[context.Path.String()]
|
||||||
|
lm.keyFields = append(lm.keyFields, fieldName)
|
||||||
|
|
||||||
|
// This tag doesn't generate any validations. It just accumulates
|
||||||
|
// information for other tags to use.
|
||||||
|
return Validations{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lmktv listMapKeyTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: lmktv.TagName(),
|
||||||
|
Scopes: lmktv.ValidScopes().UnsortedList(),
|
||||||
|
Description: "Declares a named sub-field of a list's value-type to be part of the list-map key.",
|
||||||
|
Payloads: []TagPayloadDoc{{
|
||||||
|
Description: "<field-json-name>",
|
||||||
|
Docs: "The name of the field.",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
type eachValTagValidator struct {
|
||||||
|
byFieldPath map[string]*listMap
|
||||||
|
validator Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evtv *eachValTagValidator) Init(cfg Config) {
|
||||||
|
evtv.validator = cfg.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eachValTagValidator) TagName() string {
|
||||||
|
return eachValTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eachValTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return listTagsValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// LateTagValidator indicatesa that validator has to run after the listType and
|
||||||
|
// listMapKey tags.
|
||||||
|
func (eachValTagValidator) LateTagValidator() {}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validateEachSliceVal = types.Name{Package: libValidationPkg, Name: "EachSliceVal"}
|
||||||
|
validateEachSliceValNilable = types.Name{Package: libValidationPkg, Name: "EachSliceValNilable"}
|
||||||
|
validateEachMapVal = types.Name{Package: libValidationPkg, Name: "EachMapVal"}
|
||||||
|
validateEachMapValNilable = types.Name{Package: libValidationPkg, Name: "EachMapValNilable"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (evtv eachValTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
|
||||||
|
t := context.Type
|
||||||
|
if t.Kind == types.Alias {
|
||||||
|
t = t.Underlying
|
||||||
|
}
|
||||||
|
switch t.Kind {
|
||||||
|
case types.Slice, types.Array, types.Map:
|
||||||
|
default:
|
||||||
|
return Validations{}, fmt.Errorf("can only be used on list or map types")
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeComments := []string{payload}
|
||||||
|
elemContext := Context{
|
||||||
|
Type: t.Elem,
|
||||||
|
Parent: t,
|
||||||
|
Path: context.Path.Key("*"),
|
||||||
|
}
|
||||||
|
switch t.Kind {
|
||||||
|
case types.Slice, types.Array:
|
||||||
|
elemContext.Scope = ScopeListVal
|
||||||
|
case types.Map:
|
||||||
|
elemContext.Scope = ScopeMapVal
|
||||||
|
}
|
||||||
|
if validations, err := evtv.validator.ExtractValidations(elemContext, fakeComments); err != nil {
|
||||||
|
return Validations{}, err
|
||||||
|
} else {
|
||||||
|
if len(validations.Variables) > 0 {
|
||||||
|
return Validations{}, fmt.Errorf("variable generation is not supported")
|
||||||
|
}
|
||||||
|
return evtv.getValidations(context.Path, t, validations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evtv eachValTagValidator) getValidations(fldPath *field.Path, t *types.Type, validations Validations) (Validations, error) {
|
||||||
|
switch t.Kind {
|
||||||
|
case types.Slice, types.Array:
|
||||||
|
return evtv.getListValidations(fldPath, t, validations)
|
||||||
|
case types.Map:
|
||||||
|
return evtv.getMapValidations(t, validations)
|
||||||
|
}
|
||||||
|
return Validations{}, fmt.Errorf("non-iterable type: %v", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEachVal returns a validation that applies a function to each element of
|
||||||
|
// a list or map.
|
||||||
|
func ForEachVal(fldPath *field.Path, t *types.Type, fn FunctionGen) (Validations, error) {
|
||||||
|
return globalEachVal.getValidations(fldPath, t, Validations{Functions: []FunctionGen{fn}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evtv eachValTagValidator) getListValidations(fldPath *field.Path, t *types.Type, validations Validations) (Validations, error) {
|
||||||
|
result := Validations{}
|
||||||
|
result.OpaqueValType = validations.OpaqueType
|
||||||
|
|
||||||
|
var listMap *listMap
|
||||||
|
if lm, found := evtv.byFieldPath[fldPath.String()]; found {
|
||||||
|
if !lm.declaredAsMap {
|
||||||
|
return Validations{}, fmt.Errorf("found listMapKey without listType=map")
|
||||||
|
}
|
||||||
|
if len(lm.keyFields) == 0 {
|
||||||
|
return Validations{}, fmt.Errorf("found listType=map without listMapKey")
|
||||||
|
}
|
||||||
|
listMap = lm
|
||||||
|
}
|
||||||
|
for _, vfn := range validations.Functions {
|
||||||
|
// Which function we call depends on whether the value-type is
|
||||||
|
// already nilable or not.
|
||||||
|
var validateEach types.Name
|
||||||
|
|
||||||
|
if isNilableType(t.Elem) {
|
||||||
|
validateEach = validateEachSliceValNilable
|
||||||
|
} else {
|
||||||
|
validateEach = validateEachSliceVal
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmpArg any = Literal("nil")
|
||||||
|
if listMap != nil {
|
||||||
|
cmpFn := FunctionLiteral{
|
||||||
|
Parameters: []ParamResult{{"a", t.Elem}, {"b", t.Elem}},
|
||||||
|
Results: []ParamResult{{"", types.Bool}},
|
||||||
|
}
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.WriteString("return ")
|
||||||
|
for i, fld := range listMap.keyFields {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteString(" && ")
|
||||||
|
}
|
||||||
|
buf.WriteString(fmt.Sprintf("a.%s == b.%s", fld, fld))
|
||||||
|
}
|
||||||
|
cmpFn.Body = buf.String()
|
||||||
|
cmpArg = cmpFn
|
||||||
|
}
|
||||||
|
f := Function(eachValTagName, vfn.Flags(), validateEach, cmpArg, WrapperFunction{vfn, t.Elem})
|
||||||
|
result.Functions = append(result.Functions, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evtv eachValTagValidator) getMapValidations(t *types.Type, validations Validations) (Validations, error) {
|
||||||
|
result := Validations{}
|
||||||
|
result.OpaqueValType = validations.OpaqueType
|
||||||
|
|
||||||
|
for _, vfn := range validations.Functions {
|
||||||
|
// Which function we call depends on whether the value-type is
|
||||||
|
// already nilable or not.
|
||||||
|
var validateEach types.Name
|
||||||
|
|
||||||
|
if isNilableType(t.Elem) {
|
||||||
|
validateEach = validateEachMapValNilable
|
||||||
|
} else {
|
||||||
|
validateEach = validateEachMapVal
|
||||||
|
}
|
||||||
|
|
||||||
|
f := Function(eachValTagName, vfn.Flags(), validateEach, WrapperFunction{vfn, t.Elem})
|
||||||
|
result.Functions = append(result.Functions, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (evtv eachValTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: evtv.TagName(),
|
||||||
|
Scopes: evtv.ValidScopes().UnsortedList(),
|
||||||
|
Description: "Declares a validation for each value in a map or list.",
|
||||||
|
Payloads: []TagPayloadDoc{{
|
||||||
|
Description: "<validation-tag>",
|
||||||
|
Docs: "The tag to evaluate for each value.",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
type eachKeyTagValidator struct {
|
||||||
|
validator Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ektv *eachKeyTagValidator) Init(cfg Config) {
|
||||||
|
ektv.validator = cfg.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eachKeyTagValidator) TagName() string {
|
||||||
|
return eachKeyTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eachKeyTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return listTagsValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validateEachMapKey = types.Name{Package: libValidationPkg, Name: "EachMapKey"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ektv eachKeyTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
|
||||||
|
t := context.Type
|
||||||
|
if t.Kind == types.Alias {
|
||||||
|
t = t.Underlying
|
||||||
|
}
|
||||||
|
if t.Kind != types.Map {
|
||||||
|
return Validations{}, fmt.Errorf("can only be used on map types")
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeComments := []string{payload}
|
||||||
|
elemContext := Context{
|
||||||
|
Scope: ScopeMapKey,
|
||||||
|
Type: t.Elem,
|
||||||
|
Parent: t,
|
||||||
|
Path: context.Path.Child("(keys)"),
|
||||||
|
}
|
||||||
|
if validations, err := ektv.validator.ExtractValidations(elemContext, fakeComments); err != nil {
|
||||||
|
return Validations{}, err
|
||||||
|
} else {
|
||||||
|
if len(validations.Variables) > 0 {
|
||||||
|
return Validations{}, fmt.Errorf("variable generation is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ektv.getValidations(t, validations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ektv eachKeyTagValidator) getValidations(t *types.Type, validations Validations) (Validations, error) {
|
||||||
|
result := Validations{}
|
||||||
|
result.OpaqueKeyType = validations.OpaqueType
|
||||||
|
for _, vfn := range validations.Functions {
|
||||||
|
f := Function(eachKeyTagName, vfn.Flags(), validateEachMapKey, WrapperFunction{vfn, t.Key})
|
||||||
|
result.Functions = append(result.Functions, f)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEachKey returns a validation that applies a function to each key of
|
||||||
|
// a map.
|
||||||
|
func ForEachKey(_ *field.Path, t *types.Type, fn FunctionGen) (Validations, error) {
|
||||||
|
return globalEachKey.getValidations(t, Validations{Functions: []FunctionGen{fn}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ektv eachKeyTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: ektv.TagName(),
|
||||||
|
Scopes: ektv.ValidScopes().UnsortedList(),
|
||||||
|
Description: "Declares a validation for each value in a map or list.",
|
||||||
|
Payloads: []TagPayloadDoc{{
|
||||||
|
Description: "<validation-tag>",
|
||||||
|
Docs: "The tag to evaluate for each value.",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/gengo/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
subfieldTagName = "k8s:subfield"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterTagValidator(&subfieldTagValidator{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type subfieldTagValidator struct {
|
||||||
|
validator Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stv *subfieldTagValidator) Init(cfg Config) {
|
||||||
|
stv.validator = cfg.Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (subfieldTagValidator) TagName() string {
|
||||||
|
return subfieldTagName
|
||||||
|
}
|
||||||
|
|
||||||
|
var subfieldTagValidScopes = sets.New(ScopeAny)
|
||||||
|
|
||||||
|
func (subfieldTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return subfieldTagValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validateSubfield = types.Name{Package: libValidationPkg, Name: "Subfield"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (stv subfieldTagValidator) GetValidations(context Context, args []string, payload string) (Validations, error) {
|
||||||
|
t := realType(context.Type)
|
||||||
|
if t.Kind != types.Struct {
|
||||||
|
return Validations{}, fmt.Errorf("can only be used on struct types")
|
||||||
|
}
|
||||||
|
if len(args) != 1 {
|
||||||
|
return Validations{}, fmt.Errorf("requires exactly one arg")
|
||||||
|
}
|
||||||
|
subname := args[0]
|
||||||
|
submemb := getMemberByJSON(t, subname)
|
||||||
|
if submemb == nil {
|
||||||
|
return Validations{}, fmt.Errorf("no field for json name %q", subname)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := Validations{}
|
||||||
|
|
||||||
|
fakeComments := []string{payload}
|
||||||
|
subContext := Context{
|
||||||
|
Scope: ScopeField,
|
||||||
|
Type: submemb.Type,
|
||||||
|
Parent: t,
|
||||||
|
Path: context.Path.Child(subname),
|
||||||
|
}
|
||||||
|
if validations, err := stv.validator.ExtractValidations(subContext, fakeComments); err != nil {
|
||||||
|
return Validations{}, err
|
||||||
|
} else {
|
||||||
|
if len(validations.Variables) > 0 {
|
||||||
|
return Validations{}, fmt.Errorf("variable generation is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, vfn := range validations.Functions {
|
||||||
|
nilableStructType := context.Type
|
||||||
|
if !isNilableType(nilableStructType) {
|
||||||
|
nilableStructType = types.PointerTo(nilableStructType)
|
||||||
|
}
|
||||||
|
nilableFieldType := submemb.Type
|
||||||
|
fieldExprPrefix := ""
|
||||||
|
if !isNilableType(nilableFieldType) {
|
||||||
|
nilableFieldType = types.PointerTo(nilableFieldType)
|
||||||
|
fieldExprPrefix = "&"
|
||||||
|
}
|
||||||
|
|
||||||
|
getFn := FunctionLiteral{
|
||||||
|
Parameters: []ParamResult{{"o", nilableStructType}},
|
||||||
|
Results: []ParamResult{{"", nilableFieldType}},
|
||||||
|
}
|
||||||
|
getFn.Body = fmt.Sprintf("return %so.%s", fieldExprPrefix, submemb.Name)
|
||||||
|
f := Function(subfieldTagName, vfn.Flags(), validateSubfield, subname, getFn, WrapperFunction{vfn, submemb.Type})
|
||||||
|
result.Functions = append(result.Functions, f)
|
||||||
|
result.Variables = append(result.Variables, validations.Variables...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stv subfieldTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: stv.TagName(),
|
||||||
|
Scopes: stv.ValidScopes().UnsortedList(),
|
||||||
|
Description: "Declares a validation for a subfield of a struct.",
|
||||||
|
Args: []TagArgDoc{{
|
||||||
|
Description: "<field-json-name>",
|
||||||
|
}},
|
||||||
|
Docs: "The named subfield must be a direct field of the struct, or of an embedded struct.",
|
||||||
|
Payloads: []TagPayloadDoc{{
|
||||||
|
Description: "<validation-tag>",
|
||||||
|
Docs: "The tag to evaluate for the subfield.",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user