mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 15:58:37 +00:00
Add validators: optional/required/forbidden
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
31f4637217
commit
63050550c3
133
staging/src/k8s.io/apimachinery/pkg/api/validate/required.go
Normal file
133
staging/src/k8s.io/apimachinery/pkg/api/validate/required.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequiredValue verifies that the specified value is not the zero-value for
|
||||||
|
// its type.
|
||||||
|
func RequiredValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
var zero T
|
||||||
|
if *value != zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredPointer verifies that the specified pointer is not nil.
|
||||||
|
func RequiredPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
if value != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredSlice verifies that the specified slice is not empty.
|
||||||
|
func RequiredSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||||
|
if len(value) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredMap verifies that the specified map is not empty.
|
||||||
|
func RequiredMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||||
|
if len(value) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForbiddenValue verifies that the specified value is the zero-value for its
|
||||||
|
// type.
|
||||||
|
func ForbiddenValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
var zero T
|
||||||
|
if *value == zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForbiddenPointer verifies that the specified pointer is nil.
|
||||||
|
func ForbiddenPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForbiddenSlice verifies that the specified slice is empty.
|
||||||
|
func ForbiddenSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredMap verifies that the specified map is empty.
|
||||||
|
func ForbiddenMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Forbidden(fldPath, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalValue verifies that the specified value is not the zero-value for
|
||||||
|
// its type. This is identical to RequiredValue, but the caller should treat an
|
||||||
|
// error here as an indication that the optional value was not specified.
|
||||||
|
func OptionalValue[T comparable](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
var zero T
|
||||||
|
if *value != zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalPointer verifies that the specified pointer is not nil. This is
|
||||||
|
// identical to RequiredPointer, but the caller should treat an error here as an
|
||||||
|
// indication that the optional value was not specified.
|
||||||
|
func OptionalPointer[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
|
||||||
|
if value != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalSlice verifies that the specified slice is not empty. This is
|
||||||
|
// identical to RequiredSlice, but the caller should treat an error here as an
|
||||||
|
// indication that the optional value was not specified.
|
||||||
|
func OptionalSlice[T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList {
|
||||||
|
if len(value) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalMap verifies that the specified map is not empty. This is identical
|
||||||
|
// to RequiredMap, but the caller should treat an error here as an indication that
|
||||||
|
// the optional value was not specified.
|
||||||
|
func OptionalMap[K comparable, T any](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ map[K]T) field.ErrorList {
|
||||||
|
if len(value) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return field.ErrorList{field.Required(fldPath, "optional value was not specified")}
|
||||||
|
}
|
@ -0,0 +1,924 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/operation"
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRequiredValue(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := "value"
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := "" // zero-value
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 123
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0 // zero-value
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := true
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false // zero-value
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{"value"}
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{} // zero-value
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ptr.To("")
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil) // zero-value
|
||||||
|
return RequiredValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredPointer(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ""
|
||||||
|
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*string)(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0
|
||||||
|
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*int)(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false
|
||||||
|
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*bool)(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{}
|
||||||
|
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*struct{ S string })(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (**string)(nil)
|
||||||
|
return RequiredPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredSlice(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{""}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{0}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{false}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{nil}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{}
|
||||||
|
return RequiredSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredMap(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{"": ""}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{0: 0}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[bool]bool{false: false}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]bool{}
|
||||||
|
return RequiredMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Required value",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalValue(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := "value"
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := "" // zero-value
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 123
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0 // zero-value
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := true
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false // zero-value
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{"value"}
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{} // zero-value
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ptr.To("")
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil) // zero-value
|
||||||
|
return OptionalValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalPointer(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ""
|
||||||
|
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*string)(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0
|
||||||
|
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*int)(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false
|
||||||
|
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*bool)(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{}
|
||||||
|
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*struct{ S string })(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (**string)(nil)
|
||||||
|
return OptionalPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalSlice(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{""}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{0}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{false}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{nil}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{}
|
||||||
|
return OptionalSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalMap(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{"": ""}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{0: 0}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[bool]bool{false: false}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]bool{}
|
||||||
|
return OptionalMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath:.*optional value was not specified",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbiddenValue(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ""
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := "value"
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 123
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := true
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{}
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{"value"}
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil)
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ptr.To("")
|
||||||
|
return ForbiddenValue(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbiddenPointer(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*string)(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := ""
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*int)(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := 0
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*bool)(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := false
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (*struct{ S string })(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := struct{ S string }{}
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
pointer := (**string)(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, pointer, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := (*string)(nil)
|
||||||
|
return ForbiddenPointer(context.Background(), op, fp, &value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbiddenSlice(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []string{""}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []int{0}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []bool{false}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := []*string{nil}
|
||||||
|
return ForbiddenSlice(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbiddenMap(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
fn func(op operation.Operation, fp *field.Path) field.ErrorList
|
||||||
|
err string // regex
|
||||||
|
}{{
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]string{"": ""}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[int]int{0: 0}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[string]bool{}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
fn: func(op operation.Operation, fp *field.Path) field.ErrorList {
|
||||||
|
value := map[bool]bool{false: false}
|
||||||
|
return ForbiddenMap(context.Background(), op, fp, value, nil)
|
||||||
|
},
|
||||||
|
err: "fldpath: Forbidden",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
result := tc.fn(operation.Operation{}, field.NewPath("fldpath"))
|
||||||
|
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: expected %q", i, tc.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
if len(result) > 1 {
|
||||||
|
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if re := regexp.MustCompile(tc.err); !re.MatchString(result[0].Error()) {
|
||||||
|
t.Errorf("case %d: wrong error\nexpected: %q\n got: %v", i, tc.err, fmtErrs(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"bytes"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fmtErrs is a helper for nicer test output. It will use multiple lines if
|
||||||
|
// errs has more than 1 item.
|
||||||
|
func fmtErrs(errs field.ErrorList) string {
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return "<no errors>"
|
||||||
|
}
|
||||||
|
if len(errs) == 1 {
|
||||||
|
return strconv.Quote(errs[0].Error())
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
for _, e := range errs {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
buf.WriteString(strconv.Quote(e.Error()))
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/gengo/v2/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
requiredTagName = "k8s:required"
|
||||||
|
optionalTagName = "k8s:optional"
|
||||||
|
forbiddenTagName = "k8s:forbidden"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterTagValidator(requirednessTagValidator{requirednessRequired})
|
||||||
|
RegisterTagValidator(requirednessTagValidator{requirednessOptional})
|
||||||
|
RegisterTagValidator(requirednessTagValidator{requirednessForbidden})
|
||||||
|
}
|
||||||
|
|
||||||
|
// requirednessTagValidator implements multiple modes of requiredness.
|
||||||
|
type requirednessTagValidator struct {
|
||||||
|
mode requirednessMode
|
||||||
|
}
|
||||||
|
|
||||||
|
type requirednessMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
requirednessRequired requirednessMode = requiredTagName
|
||||||
|
requirednessOptional requirednessMode = optionalTagName
|
||||||
|
requirednessForbidden requirednessMode = forbiddenTagName
|
||||||
|
)
|
||||||
|
|
||||||
|
func (requirednessTagValidator) Init(_ Config) {}
|
||||||
|
|
||||||
|
func (rtv requirednessTagValidator) TagName() string {
|
||||||
|
return string(rtv.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var requirednessTagValidScopes = sets.New(ScopeField)
|
||||||
|
|
||||||
|
func (requirednessTagValidator) ValidScopes() sets.Set[Scope] {
|
||||||
|
return requirednessTagValidScopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtv requirednessTagValidator) GetValidations(context Context, _ []string, _ string) (Validations, error) {
|
||||||
|
if context.Type.Kind == types.Alias {
|
||||||
|
panic("alias type should already have been unwrapped")
|
||||||
|
}
|
||||||
|
switch rtv.mode {
|
||||||
|
case requirednessRequired:
|
||||||
|
return rtv.doRequired(context)
|
||||||
|
case requirednessOptional:
|
||||||
|
return rtv.doOptional(context)
|
||||||
|
case requirednessForbidden:
|
||||||
|
return rtv.doForbidden(context)
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("unknown requiredness mode: %q", rtv.mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
requiredValueValidator = types.Name{Package: libValidationPkg, Name: "RequiredValue"}
|
||||||
|
requiredPointerValidator = types.Name{Package: libValidationPkg, Name: "RequiredPointer"}
|
||||||
|
requiredSliceValidator = types.Name{Package: libValidationPkg, Name: "RequiredSlice"}
|
||||||
|
requiredMapValidator = types.Name{Package: libValidationPkg, Name: "RequiredMap"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: It might be valuable to have a string payload for when requiredness is
|
||||||
|
// conditional (e.g. required when <otherfield> is specified).
|
||||||
|
func (requirednessTagValidator) doRequired(context Context) (Validations, error) {
|
||||||
|
// Most validators don't care whether the value they are validating was
|
||||||
|
// originally defined as a value-type or a pointer-type in the API. This
|
||||||
|
// one does. Since Go doesn't do partial specialization of templates, we
|
||||||
|
// do manual dispatch here.
|
||||||
|
switch context.Type.Kind {
|
||||||
|
case types.Slice:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredSliceValidator)}}, nil
|
||||||
|
case types.Map:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredMapValidator)}}, nil
|
||||||
|
case types.Pointer:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredPointerValidator)}}, nil
|
||||||
|
case types.Struct:
|
||||||
|
// The +required tag on a non-pointer struct is only for documentation.
|
||||||
|
// We don't perform validation here and defer the validation to
|
||||||
|
// the struct's fields.
|
||||||
|
return Validations{Comments: []string{"required non-pointer structs are purely documentation"}}, nil
|
||||||
|
}
|
||||||
|
return Validations{Functions: []FunctionGen{Function(requiredTagName, ShortCircuit, requiredValueValidator)}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
optionalValueValidator = types.Name{Package: libValidationPkg, Name: "OptionalValue"}
|
||||||
|
optionalPointerValidator = types.Name{Package: libValidationPkg, Name: "OptionalPointer"}
|
||||||
|
optionalSliceValidator = types.Name{Package: libValidationPkg, Name: "OptionalSlice"}
|
||||||
|
optionalMapValidator = types.Name{Package: libValidationPkg, Name: "OptionalMap"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (requirednessTagValidator) doOptional(context Context) (Validations, error) {
|
||||||
|
// Most validators don't care whether the value they are validating was
|
||||||
|
// originally defined as a value-type or a pointer-type in the API. This
|
||||||
|
// one does. Since Go doesn't do partial specialization of templates, we
|
||||||
|
// do manual dispatch here.
|
||||||
|
switch context.Type.Kind {
|
||||||
|
case types.Slice:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalSliceValidator)}}, nil
|
||||||
|
case types.Map:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalMapValidator)}}, nil
|
||||||
|
case types.Pointer:
|
||||||
|
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalPointerValidator)}}, nil
|
||||||
|
case types.Struct:
|
||||||
|
// Specifying that a non-pointer struct is optional doesn't actually
|
||||||
|
// make sense technically almost ever, and is better described as a
|
||||||
|
// union inside the struct. It does, however, make sense as
|
||||||
|
// documentation.
|
||||||
|
return Validations{Comments: []string{"optional non-pointer structs are purely documentation"}}, nil
|
||||||
|
}
|
||||||
|
return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalValueValidator)}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
forbiddenValueValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenValue"}
|
||||||
|
forbiddenPointerValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenPointer"}
|
||||||
|
forbiddenSliceValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenSlice"}
|
||||||
|
forbiddenMapValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenMap"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: It might be valuable to have a string payload for when forbidden is
|
||||||
|
// conditional (e.g. forbidden when <option> is disabled).
|
||||||
|
func (requirednessTagValidator) doForbidden(context Context) (Validations, error) {
|
||||||
|
// Forbidden is weird. Each of these emits two checks, which are polar
|
||||||
|
// opposites. If the field fails the forbidden check, it will
|
||||||
|
// short-circuit and not run the optional check. If it passes the
|
||||||
|
// forbidden check, it must not be specified, so it will "fail" the
|
||||||
|
// optional check and short-circuit (but without error). Why? For
|
||||||
|
// example, this prevents any further validation from trying to run on a
|
||||||
|
// nil pointer.
|
||||||
|
switch context.Type.Kind {
|
||||||
|
case types.Slice:
|
||||||
|
return Validations{
|
||||||
|
Functions: []FunctionGen{
|
||||||
|
Function(forbiddenTagName, ShortCircuit, forbiddenSliceValidator),
|
||||||
|
Function(forbiddenTagName, ShortCircuit|NonError, optionalSliceValidator),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case types.Map:
|
||||||
|
return Validations{
|
||||||
|
Functions: []FunctionGen{
|
||||||
|
Function(forbiddenTagName, ShortCircuit, forbiddenMapValidator),
|
||||||
|
Function(forbiddenTagName, ShortCircuit|NonError, optionalMapValidator),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case types.Pointer:
|
||||||
|
return Validations{
|
||||||
|
Functions: []FunctionGen{
|
||||||
|
Function(forbiddenTagName, ShortCircuit, forbiddenPointerValidator),
|
||||||
|
Function(forbiddenTagName, ShortCircuit|NonError, optionalPointerValidator),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
case types.Struct:
|
||||||
|
// The +forbidden tag on a non-pointer struct is not supported.
|
||||||
|
// If you encounter this error and believe you have a valid use case
|
||||||
|
// for forbiddening a non-pointer struct, please let us know! We need
|
||||||
|
// to understand your scenario to determine if we need to adjust
|
||||||
|
// this behavior or provide alternative validation mechanisms.
|
||||||
|
return Validations{}, fmt.Errorf("non-pointer structs cannot use the %q tag", forbiddenTagName)
|
||||||
|
}
|
||||||
|
return Validations{
|
||||||
|
Functions: []FunctionGen{
|
||||||
|
Function(forbiddenTagName, ShortCircuit, forbiddenValueValidator),
|
||||||
|
Function(forbiddenTagName, ShortCircuit|NonError, optionalValueValidator),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rtv requirednessTagValidator) Docs() TagDoc {
|
||||||
|
doc := TagDoc{
|
||||||
|
Tag: rtv.TagName(),
|
||||||
|
Scopes: rtv.ValidScopes().UnsortedList(),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rtv.mode {
|
||||||
|
case requirednessRequired:
|
||||||
|
doc.Description = "Indicates that a field must be specified by clients."
|
||||||
|
case requirednessOptional:
|
||||||
|
doc.Description = "Indicates that a field is optional to clients."
|
||||||
|
case requirednessForbidden:
|
||||||
|
doc.Description = "Indicates that a field may not be specified."
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown requiredness mode: %q", rtv.mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user