| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- /*
- 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"
- "sort"
- "k8s.io/apimachinery/pkg/api/equality"
- "k8s.io/apimachinery/pkg/api/operation"
- "k8s.io/apimachinery/pkg/util/validation/field"
- )
- // MatchFunc is a function that compares two values of the same type,
- // according to some criteria, and returns true if they match.
- type MatchFunc[T any] func(T, T) bool
- // EachSliceVal performs validation on each element of newSlice using the provided validation function.
- //
- // For update operations, the match function finds corresponding values in oldSlice for each
- // value in newSlice. This comparison can be either full or partial (e.g., matching only
- // specific struct fields that serve as a unique identifier). If match is nil, validation
- // proceeds without considering old values, and the equiv function is not used.
- //
- // For update operations, the equiv function checks if a new value is equivalent to its
- // corresponding old value, enabling validation ratcheting. If equiv is nil but match is
- // provided, the match function is assumed to perform full value comparison.
- //
- // Note: The slice element type must be non-nilable.
- func EachSliceVal[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, newSlice, oldSlice []T,
- match, equiv MatchFunc[T], validator ValidateFunc[*T]) field.ErrorList {
- var errs field.ErrorList
- for i, val := range newSlice {
- var old *T
- if match != nil && len(oldSlice) > 0 {
- old = lookup(oldSlice, val, match)
- }
- // If the operation is an update, for validation ratcheting, skip re-validating if the old
- // value exists and either:
- // 1. The match function provides full comparison (equiv is nil)
- // 2. The equiv function confirms the values are equivalent (either directly or semantically)
- //
- // The equiv function provides equality comparison when match uses partial comparison.
- if op.Type == operation.Update && old != nil && (equiv == nil || equiv(val, *old)) {
- continue
- }
- 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, match MatchFunc[T]) *T {
- for i := range list {
- if match(list[i], target) {
- return &list[i]
- }
- }
- return nil
- }
- // EachMapVal validates each value in newMap using the specified validation
- // function, passing the corresponding old value from oldMap if the key exists in oldMap.
- // For update operations, it implements validation ratcheting by skipping validation
- // when the old value exists and the equiv function confirms the values are equivalent.
- // The value-type of the map is assumed to not be nilable.
- // If equiv is nil, value-based ratcheting is disabled and all values will be validated.
- func EachMapVal[K ~string, V any](ctx context.Context, op operation.Operation, fldPath *field.Path, newMap, oldMap map[K]V,
- equiv MatchFunc[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
- }
- // If the operation is an update, for validation ratcheting, skip re-validating if the old
- // value is found and the equiv function confirms the values are equivalent.
- if op.Type == operation.Update && old != nil && equiv != nil && equiv(val, *old) {
- continue
- }
- 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.
- 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 {
- var old *K
- if _, found := oldMap[key]; found {
- old = &key
- }
- // If the operation is an update, for validation ratcheting, skip re-validating if
- // the key is found in oldMap.
- if op.Type == operation.Update && old != nil {
- continue
- }
- // Note: the field path is the field, not the key.
- errs = append(errs, validator(ctx, op, fldPath, &key, nil)...)
- }
- return errs
- }
- // Unique verifies that each element of newSlice is unique, according to the
- // match function. It compares every element of the slice with every other
- // element and returns errors for non-unique items.
- func Unique[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, newSlice, _ []T, match MatchFunc[T]) field.ErrorList {
- var dups []int
- for i, val := range newSlice {
- for j := i + 1; j < len(newSlice); j++ {
- other := newSlice[j]
- if match(val, other) {
- if dups == nil {
- dups = make([]int, 0, len(newSlice))
- }
- if lookup(dups, j, func(a, b int) bool { return a == b }) == nil {
- dups = append(dups, j)
- }
- }
- }
- }
- var errs field.ErrorList
- sort.Ints(dups)
- for _, i := range dups {
- var val any = newSlice[i]
- // TODO: we don't want the whole item to be logged in the error, just
- // the key(s). Unfortunately, the way errors are rendered, it comes out
- // as something like "map[string]any{...}" which is not very nice. Once
- // that is fixed, we can consider adding a way for this function to
- // specify that just the keys should be rendered in the error.
- errs = append(errs, field.Duplicate(fldPath.Index(i), val))
- }
- return errs
- }
- // SemanticDeepEqual is a MatchFunc that uses equality.Semantic.DeepEqual to
- // compare two values.
- // This wrapper is needed because MatchFunc requires a function that takes two
- // arguments of specific type T, while equality.Semantic.DeepEqual takes
- // arguments of type interface{}/any. The wrapper satisfies the type
- // constraints of MatchFunc while leveraging the underlying semantic equality
- // logic. It can be used by any other function that needs to call DeepEqual.
- func SemanticDeepEqual[T any](a, b T) bool {
- return equality.Semantic.DeepEqual(a, b)
- }
- // DirectEqual is a MatchFunc that uses the == operator to compare two values.
- // It can be used by any other function that needs to compare two values
- // directly.
- func DirectEqual[T comparable](a, b T) bool {
- return a == b
- }
- // DirectEqualPtr is a MatchFunc that dereferences two pointers and uses the ==
- // operator to compare the values. If both pointers are nil, it returns true.
- // If one pointer is nil and the other is not, it returns false.
- // It can be used by any other function that needs to compare two pointees
- // directly.
- func DirectEqualPtr[T comparable](a, b *T) bool {
- if a == b {
- return true
- }
- if a == nil || b == nil {
- return false
- }
- return *a == *b
- }
|