mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-11 13:02:14 +00:00
Add validation-gen test infrastructure
Introduces the infrastructure for testing validation-gen tags. 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
3210f46b5b
commit
8c41bdf05b
64
staging/src/k8s.io/apimachinery/pkg/api/validate/README.md
Normal file
64
staging/src/k8s.io/apimachinery/pkg/api/validate/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# API validation
|
||||
|
||||
This package holds functions which validate fields and types in the Kubernetes
|
||||
API. It may be useful beyond API validation, but this is the primary goal.
|
||||
|
||||
Most of the public functions here have signatures which adhere to the following
|
||||
pattern, which is assumed by automation and code-generation:
|
||||
|
||||
```
|
||||
import (
|
||||
"context"
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func <Name>(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue <ValueType>, <OtherArgs...>) field.ErrorList
|
||||
```
|
||||
|
||||
The name of validator functions should consider that callers will generally be
|
||||
spelling out the package name and the function name, and so should aim for
|
||||
legibility. E.g. `validate.Concept()`.
|
||||
|
||||
The `ctx` argument is Go's usual Context.
|
||||
|
||||
The `opCtx` argument provides information about the API operation in question.
|
||||
|
||||
The `fldPath` argument indicates the path to the field in question, to be used
|
||||
in errors.
|
||||
|
||||
The `value` and `oldValue` arguments are the thing(s) being validated. For
|
||||
CREATE operations (`opCtx.Operation == operation.Create`), the `oldValue`
|
||||
argument will be nil. Many validators functions only look at the current value
|
||||
(`value`) and disregard `oldValue`.
|
||||
|
||||
The `value` and `oldValue` arguments are always nilable - pointers to primitive
|
||||
types, slices of any type, or maps of any type. Validator functions should
|
||||
avoid dereferencing nil. Callers are expected to not pass a nil `value` unless the
|
||||
API field itself was nilable. `oldValue` is always nil for CREATE operations and
|
||||
is also nil for UPDATE operations if the `value` is not correlated with an `oldValue`.
|
||||
|
||||
Simple content-validators may have no `<OtherArgs>`, but validator functions
|
||||
may take additional arguments. Some validator functions will be built as
|
||||
generics, e.g. to allow any integer type or to handle arbitrary slices.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
// NonEmpty validates that a string is not empty.
|
||||
func NonEmpty(ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *string) field.ErrorList
|
||||
|
||||
// Even validates that a slice has an even number of items.
|
||||
func Even[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ []T) field.ErrorList
|
||||
|
||||
// KeysMaxLen validates that all of the string keys in a map are under the
|
||||
// specified length.
|
||||
func KeysMaxLen[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ map[string]T, maxLen int) field.ErrorList
|
||||
```
|
||||
|
||||
Validator functions always return an `ErrorList` where each item is a distinct
|
||||
validation failure and a zero-length return value (not just nil) indicates
|
||||
success.
|
||||
|
||||
Good validation failure messages follow the Kubernetes API conventions, for
|
||||
example using "must" instead of "should".
|
50
staging/src/k8s.io/apimachinery/pkg/api/validate/doc.go
Normal file
50
staging/src/k8s.io/apimachinery/pkg/api/validate/doc.go
Normal file
@ -0,0 +1,50 @@
|
||||
/*
|
||||
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 holds API validation functions which are designed for use
|
||||
// with the k8s.io/code-generator/cmd/validation-gen tool. Each validation
|
||||
// function has a similar fingerprint:
|
||||
//
|
||||
// func <Name>(ctx context.Context,
|
||||
// op operation.Operation,
|
||||
// fldPath *field.Path,
|
||||
// value, oldValue <nilable type>,
|
||||
// <other args...>) field.ErrorList
|
||||
//
|
||||
// The value and oldValue arguments will always be a nilable type. If the
|
||||
// original value was a string, these will be a *string. If the original value
|
||||
// was a slice or map, these will be the same slice or map type.
|
||||
//
|
||||
// For a CREATE operation, the oldValue will always be nil. For an UPDATE
|
||||
// operation, either value or oldValue may be nil, e.g. when adding or removing
|
||||
// a value in a list-map. Validators which care about UPDATE operations should
|
||||
// look at the opCtx argument to know which operation is being executed.
|
||||
//
|
||||
// Tightened validation (also known as ratcheting validation) is supported by
|
||||
// defining a new validation function. For example:
|
||||
//
|
||||
// func TightenedMaxLength(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList {
|
||||
// if oldValue != nil && len(MaxLength(ctx, op, fldPath, oldValue, nil)) > 0 {
|
||||
// // old value is not valid, so this value skips the tightened validation
|
||||
// return nil
|
||||
// }
|
||||
// return MaxLength(ctx, op, fldPath, value, nil)
|
||||
// }
|
||||
//
|
||||
// In general, we cannot distinguish a non-specified slice or map from one that
|
||||
// is specified but empty. Validators should not rely on nil values, but use
|
||||
// len() instead.
|
||||
package validate
|
35
staging/src/k8s.io/apimachinery/pkg/api/validate/testing.go
Normal file
35
staging/src/k8s.io/apimachinery/pkg/api/validate/testing.go
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2014 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"
|
||||
)
|
||||
|
||||
// FixedResult asserts a fixed boolean result. This is mostly useful for
|
||||
// testing.
|
||||
func FixedResult[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ T, result bool, arg string) field.ErrorList {
|
||||
if result {
|
||||
return nil
|
||||
}
|
||||
return field.ErrorList{
|
||||
field.Invalid(fldPath, value, "forced failure: "+arg),
|
||||
}
|
||||
}
|
144
staging/src/k8s.io/apimachinery/pkg/api/validate/testing_test.go
Normal file
144
staging/src/k8s.io/apimachinery/pkg/api/validate/testing_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
/*
|
||||
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"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestFixedResult(t *testing.T) {
|
||||
cases := []struct {
|
||||
value any
|
||||
pass bool
|
||||
}{{
|
||||
value: "",
|
||||
pass: false,
|
||||
}, {
|
||||
value: "",
|
||||
pass: true,
|
||||
}, {
|
||||
value: "nonempty",
|
||||
pass: false,
|
||||
}, {
|
||||
value: "nonempty",
|
||||
pass: true,
|
||||
}, {
|
||||
value: 0,
|
||||
pass: false,
|
||||
}, {
|
||||
value: 0,
|
||||
pass: true,
|
||||
}, {
|
||||
value: 1,
|
||||
pass: false,
|
||||
}, {
|
||||
value: 1,
|
||||
pass: true,
|
||||
}, {
|
||||
value: false,
|
||||
pass: false,
|
||||
}, {
|
||||
value: false,
|
||||
pass: true,
|
||||
}, {
|
||||
value: true,
|
||||
pass: false,
|
||||
}, {
|
||||
value: true,
|
||||
pass: true,
|
||||
}, {
|
||||
value: nil,
|
||||
pass: false,
|
||||
}, {
|
||||
value: nil,
|
||||
pass: true,
|
||||
}, {
|
||||
value: ptr.To(""),
|
||||
pass: false,
|
||||
}, {
|
||||
value: ptr.To(""),
|
||||
pass: true,
|
||||
}, {
|
||||
value: ptr.To("nonempty"),
|
||||
pass: false,
|
||||
}, {
|
||||
value: ptr.To("nonempty"),
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string(nil),
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string(nil),
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string{},
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string{},
|
||||
pass: true,
|
||||
}, {
|
||||
value: []string{"s"},
|
||||
pass: false,
|
||||
}, {
|
||||
value: []string{"s"},
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string(nil),
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string(nil),
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string{},
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string{},
|
||||
pass: true,
|
||||
}, {
|
||||
value: map[string]string{"k": "v"},
|
||||
pass: false,
|
||||
}, {
|
||||
value: map[string]string{"k": "v"},
|
||||
pass: true,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := FixedResult(context.Background(), operation.Operation{}, field.NewPath("fldpath"), tc.value, nil, tc.pass, "detail string")
|
||||
if len(result) != 0 && tc.pass {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && !tc.pass {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if want, got := "forced failure: detail string", result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
# Tag tests
|
||||
|
||||
Tests in this directory are intended to validate specific tags, rather than
|
||||
general behavior of the code generator. Some tags are deeply integrated into
|
||||
the code-generation and will end up with similar tests elsewhere.
|
||||
|
||||
These test cases should be as focused as possible.
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// +k8s:validation-gen=TypeMeta
|
||||
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
|
||||
|
||||
// This is a test package.
|
||||
package validatefalse
|
||||
|
||||
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
|
||||
var localSchemeBuilder = testscheme.New()
|
||||
|
||||
type Struct struct {
|
||||
TypeMeta int
|
||||
|
||||
// +k8s:validateFalse="field Struct.StringField"
|
||||
StringField string `json:"stringField"`
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
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 validatefalse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
st := localSchemeBuilder.Test(t)
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero-values.
|
||||
}).ExpectValidateFalseByPath(map[string][]string{
|
||||
"stringField": {"field Struct.StringField"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
StringField: "abc",
|
||||
}).ExpectValidateFalseByPath(map[string][]string{
|
||||
"stringField": {"field Struct.StringField"},
|
||||
})
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by validation-gen. DO NOT EDIT.
|
||||
|
||||
package validatefalse
|
||||
|
||||
import (
|
||||
context "context"
|
||||
fmt "fmt"
|
||||
|
||||
operation "k8s.io/apimachinery/pkg/api/operation"
|
||||
safe "k8s.io/apimachinery/pkg/api/safe"
|
||||
validate "k8s.io/apimachinery/pkg/api/validate"
|
||||
field "k8s.io/apimachinery/pkg/util/validation/field"
|
||||
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
)
|
||||
|
||||
func init() { localSchemeBuilder.Register(RegisterValidations) }
|
||||
|
||||
// RegisterValidations adds validation functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterValidations(scheme *testscheme.Scheme) error {
|
||||
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}, subresources ...string) field.ErrorList {
|
||||
if len(subresources) == 0 {
|
||||
return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj))
|
||||
}
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", obj, subresources))}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) {
|
||||
// field Struct.TypeMeta has no validation
|
||||
|
||||
// field Struct.StringField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
errs = append(errs, validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.StringField")...)
|
||||
return
|
||||
}(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }))...)
|
||||
|
||||
return errs
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
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 testscheme provides a scheme implementation and test utilities
|
||||
// useful for writing output_tests for validation-gen.
|
||||
//
|
||||
// For an output test to use this scheme, it should be located in a dedicated go package.
|
||||
// The go package should have validation-gen and this test scheme enabled and must declare a
|
||||
// 'localSchemeBuilder' using the 'testscheme.New()' function. For example:
|
||||
//
|
||||
// // +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
|
||||
// // +k8s:validation-gen
|
||||
// package example
|
||||
// import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
// var localSchemeBuilder = testscheme.New()
|
||||
//
|
||||
// This is sufficient for validation-gen to generate a `zz_generated.validations.go` for the types
|
||||
// in the package that compile.
|
||||
//
|
||||
// With the scheme enabled. An output test may be tested either by handwritten test code or
|
||||
// by generated test fixtures.
|
||||
//
|
||||
// For example, to test by hand. The testschema provides utilities to create a value and assert
|
||||
// that the expected errors are returned when the value is validated:
|
||||
//
|
||||
// func Test(t *testing.T) {
|
||||
// st := localSchemeBuilder.Test(t)
|
||||
// st.Value(&T1{
|
||||
// E0: "x",
|
||||
// PE0: pointer.To(E0("y")),
|
||||
// }).ExpectInvalid(
|
||||
// field.NotSupported(field.NewPath("e0"), "x", []string{EnumValue1, EnumValue2}),
|
||||
// field.NotSupported(field.NewPath("pe0"), "y", []string{EnumValue1, EnumValue2}))}
|
||||
// }
|
||||
//
|
||||
// Tests fixtures can also be enabled. For example:
|
||||
//
|
||||
// // ...
|
||||
// // +k8s:validation-gen-test-fixture=validateFalse
|
||||
// // package example
|
||||
//
|
||||
// When a test fixture is enabled, a `zz_generated.validations_test.go` file will be generated
|
||||
// test for all the registered types in the package according to the behavior of the named test
|
||||
// fixture(s).
|
||||
//
|
||||
// Test Fixtures:
|
||||
//
|
||||
// `validateFalse` - This test fixture executes validation of each registered type and accumulates
|
||||
// all `validateFalse` validation errors. For example:
|
||||
//
|
||||
// type T1 struct {
|
||||
// // +k8s:validateFalse="field T1.S"
|
||||
// S string `json:"s"`
|
||||
// // +k8s:validateFalse="field T1.T"
|
||||
// T T2 `json:"t"`
|
||||
// }
|
||||
//
|
||||
// The above `example.T1` test type has two validated fields: `s` and 't'. The fields are named
|
||||
// according to the Go "json" field tag, and each has a validation error identifier
|
||||
// provided by `+k8s:validateFalse=<identifier>`.
|
||||
//
|
||||
// The `validateFalse` test fixture will validate an instance of `example.T1`, generated using fuzzed
|
||||
// data, and then group the validation errors by field name. Represented in JSON like:
|
||||
//
|
||||
// {
|
||||
// "*example.T1": {
|
||||
// "s": [
|
||||
// "field T1.S"
|
||||
// ],
|
||||
// "t": [
|
||||
// "field T1.T"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// This validation error data contains an object for each registered type, keyed by type name.
|
||||
// For each registered type, the validation errors from `+k8s:validateFalse` tags are grouped
|
||||
// by field path with all validation error identifiers collected into a list.
|
||||
//
|
||||
// This data is compared with the expected validation results that are defined in
|
||||
// a `testdata/validate-false.json` file in the same package, and any differences are
|
||||
// reported as test errors.
|
||||
//
|
||||
// `testdata/validate-false.json` can be generated automatically by setting the
|
||||
// `UPDATE_VALIDATION_GEN_FIXTURE_DATA=true` environment variable to true when running the tests.
|
||||
//
|
||||
// Test authors that generated `testdata/validate-false.json` are expected to ensure that file
|
||||
// is correct before checking it in to source control.
|
||||
//
|
||||
// The fuzzed data is generated pseudo-randomly with a consistent seed, with all nilable fields se
|
||||
// to a value, and with a single entry for each map and a single element for each slice.
|
||||
package testscheme
|
@ -0,0 +1,537 @@
|
||||
/*
|
||||
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 testscheme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
stdcmp "cmp"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp" // nolint:depguard // this package provides test utilities
|
||||
"github.com/google/go-cmp/cmp/cmpopts" // nolint:depguard // this package provides test utilities
|
||||
fuzz "github.com/google/gofuzz"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Scheme is similar to runtime.Scheme, but for validation testing purposes. Scheme only supports validation,
|
||||
// supports registration of any type (not just runtime.Object) and implements Register directly, allowing it
|
||||
// to also be used as a scheme builder.
|
||||
// Must only be used with tests that perform all registration before calls to validate.
|
||||
type Scheme struct {
|
||||
validationFuncs map[reflect.Type]func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList
|
||||
registrationErrors field.ErrorList
|
||||
}
|
||||
|
||||
// New creates a new Scheme.
|
||||
func New() *Scheme {
|
||||
return &Scheme{validationFuncs: map[reflect.Type]func(ctx context.Context, op operation.Operation, object interface{}, oldObject interface{}, subresources ...string) field.ErrorList{}}
|
||||
}
|
||||
|
||||
// AddValidationFunc registers a validation function.
|
||||
// Last writer wins.
|
||||
func (s *Scheme) AddValidationFunc(srcType any, fn func(ctx context.Context, op operation.Operation, object, oldObject interface{}, subresources ...string) field.ErrorList) {
|
||||
s.validationFuncs[reflect.TypeOf(srcType)] = fn
|
||||
}
|
||||
|
||||
// Validate validates an object using the registered validation function.
|
||||
func (s *Scheme) Validate(ctx context.Context, opts sets.Set[string], object any, subresources ...string) field.ErrorList {
|
||||
if len(s.registrationErrors) > 0 {
|
||||
return s.registrationErrors // short circuit with registration errors if any are present
|
||||
}
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Create, Options: opts}, object, nil, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateUpdate validates an update to an object using the registered validation function.
|
||||
func (s *Scheme) ValidateUpdate(ctx context.Context, opts sets.Set[string], object, oldObject any, subresources ...string) field.ErrorList {
|
||||
if len(s.registrationErrors) > 0 {
|
||||
return s.registrationErrors // short circuit with registration errors if any are present
|
||||
}
|
||||
if fn, ok := s.validationFuncs[reflect.TypeOf(object)]; ok {
|
||||
return fn(ctx, operation.Operation{Type: operation.Update}, object, oldObject, subresources...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register adds a scheme setup function to the list.
|
||||
func (s *Scheme) Register(funcs ...func(*Scheme) error) {
|
||||
for _, f := range funcs {
|
||||
err := f(s)
|
||||
if err != nil {
|
||||
s.registrationErrors = append(s.registrationErrors, toRegistrationError(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toRegistrationError(err error) *field.Error {
|
||||
return field.InternalError(nil, fmt.Errorf("registration error: %w", err))
|
||||
}
|
||||
|
||||
// Test returns a ValidationTestBuilder for this scheme.
|
||||
func (s *Scheme) Test(t *testing.T) *ValidationTestBuilder {
|
||||
return &ValidationTestBuilder{t, s}
|
||||
}
|
||||
|
||||
// ValidationTestBuilder provides convenience functions to build
|
||||
// validation tests.
|
||||
type ValidationTestBuilder struct {
|
||||
*testing.T
|
||||
s *Scheme
|
||||
}
|
||||
|
||||
const fixtureEnvVar = "UPDATE_VALIDATION_GEN_FIXTURE_DATA"
|
||||
|
||||
// ValidateFixtures ensures that the validation errors of all registered types match what is expected by the test fixture files.
|
||||
// For each registered type, a value is created for the type, and populated by fuzzing the value, before validating the type.
|
||||
// See ValueFuzzed for details.
|
||||
//
|
||||
// If the UPDATE_VALIDATION_GEN_FIXTURE_DATA=true environment variable is set, test fixture files are created or overridden.
|
||||
//
|
||||
// Fixtures:
|
||||
// - validate-false.json: defines a map of registered type to a map of field path to +validateFalse validations args
|
||||
// that are expected to be returned as errors when the type is validated.
|
||||
func (s *ValidationTestBuilder) ValidateFixtures() {
|
||||
s.T.Helper()
|
||||
|
||||
flag := os.Getenv(fixtureEnvVar)
|
||||
// Run validation
|
||||
got := map[string]map[string][]string{}
|
||||
for t := range s.s.validationFuncs {
|
||||
var v any
|
||||
// TODO: this should handle maps and slices
|
||||
if t.Kind() == reflect.Ptr {
|
||||
v = reflect.New(t.Elem()).Interface()
|
||||
} else {
|
||||
v = reflect.Indirect(reflect.New(t)).Interface()
|
||||
}
|
||||
if reflect.TypeOf(v).Kind() != reflect.Ptr {
|
||||
v = &v
|
||||
}
|
||||
s.ValueFuzzed(v)
|
||||
vt := &ValidationTester{ValidationTestBuilder: s, value: v}
|
||||
byPath := vt.ValidateFalseArgsByPath()
|
||||
got[t.String()] = byPath
|
||||
}
|
||||
|
||||
testdataFilename := "testdata/validate-false.json"
|
||||
if flag == "true" {
|
||||
// Generate fixture file
|
||||
if err := os.MkdirAll(path.Dir(testdataFilename), os.FileMode(0755)); err != nil {
|
||||
s.Fatal("error making directory", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(got, " ", " ")
|
||||
if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
err = os.WriteFile(testdataFilename, data, os.FileMode(0644))
|
||||
if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
// Load fixture file
|
||||
testdataFile, err := os.Open(testdataFilename)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
s.Fatalf("%s test fixture data not found. Run go test with the environment variable %s=true to create test fixture data.",
|
||||
testdataFilename, fixtureEnvVar)
|
||||
} else if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
err := testdataFile.Close()
|
||||
if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
byteValue, err := io.ReadAll(testdataFile)
|
||||
if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
testdata := map[string]map[string][]string{}
|
||||
err = json.Unmarshal(byteValue, &testdata)
|
||||
if err != nil {
|
||||
s.Fatal(err)
|
||||
}
|
||||
// Compare fixture with validation results
|
||||
expectedKeys := sets.New[string]()
|
||||
gotKeys := sets.New[string]()
|
||||
for k := range got {
|
||||
gotKeys.Insert(k)
|
||||
}
|
||||
hasErrors := false
|
||||
for k, expectedForType := range testdata {
|
||||
expectedKeys.Insert(k)
|
||||
gotForType, ok := got[k]
|
||||
s.T.Run(k, func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if !ok {
|
||||
t.Errorf("%q has expected validateFalse args in %s but got no validation errors.", k, testdataFilename)
|
||||
hasErrors = true
|
||||
} else if !cmp.Equal(gotForType, expectedForType) {
|
||||
t.Errorf("validateFalse args, grouped by field path, differed from %s:\n%s\n",
|
||||
testdataFilename, cmp.Diff(gotForType, expectedForType, cmpopts.SortMaps(stdcmp.Less[string])))
|
||||
hasErrors = true
|
||||
}
|
||||
})
|
||||
}
|
||||
for unexpectedType := range gotKeys.Difference(expectedKeys) {
|
||||
s.T.Run(unexpectedType, func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
t.Errorf("%q got unexpected validateFalse args, grouped by field path:\n%s\n",
|
||||
unexpectedType, cmp.Diff(nil, got[unexpectedType], cmpopts.SortMaps(stdcmp.Less[string])))
|
||||
hasErrors = true
|
||||
})
|
||||
}
|
||||
if hasErrors {
|
||||
s.T.Logf("If the test expectations have changed, run go test with the environment variable %s=true", fixtureEnvVar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fuzzer() *fuzz.Fuzzer {
|
||||
// Ensure that lists and maps are not empty and use a deterministic seed.
|
||||
return fuzz.New().NilChance(0.0).NumElements(2, 2).RandSource(rand.NewSource(0))
|
||||
}
|
||||
|
||||
// ValueFuzzed automatically populates the given value using a deterministic fuzzer.
|
||||
// The fuzzer sets pointers to values and always includes a two map keys and slice elements.
|
||||
func (s *ValidationTestBuilder) ValueFuzzed(value any) *ValidationTester {
|
||||
fuzzer().Fuzz(value)
|
||||
return &ValidationTester{ValidationTestBuilder: s, value: value}
|
||||
}
|
||||
|
||||
// Value returns a ValidationTester for the given value. The value
|
||||
// must be a registered with the scheme for validation.
|
||||
func (s *ValidationTestBuilder) Value(value any) *ValidationTester {
|
||||
return &ValidationTester{ValidationTestBuilder: s, value: value}
|
||||
}
|
||||
|
||||
// ValidationTester provides convenience functions to define validation
|
||||
// tests for a validatable value.
|
||||
type ValidationTester struct {
|
||||
*ValidationTestBuilder
|
||||
value any
|
||||
oldValue any
|
||||
opts sets.Set[string]
|
||||
}
|
||||
|
||||
// OldValue sets the oldValue for this ValidationTester. When oldValue is set to
|
||||
// a non-nil value, update validation will be used to test validation.
|
||||
// oldValue must be the same type as value.
|
||||
// Returns ValidationTester to support call chaining.
|
||||
func (v *ValidationTester) OldValue(oldValue any) *ValidationTester {
|
||||
v.oldValue = oldValue
|
||||
return v
|
||||
}
|
||||
|
||||
// OldValueFuzzed automatically populates the given value using a deterministic fuzzer.
|
||||
// The fuzzer sets pointers to values and always includes a two map keys and slice elements.
|
||||
func (v *ValidationTester) OldValueFuzzed(oldValue any) *ValidationTester {
|
||||
fuzzer().Fuzz(oldValue)
|
||||
v.oldValue = oldValue
|
||||
return v
|
||||
}
|
||||
|
||||
// Opts sets the ValidationOpts to use.
|
||||
func (v *ValidationTester) Opts(opts sets.Set[string]) *ValidationTester {
|
||||
v.opts = opts
|
||||
return v
|
||||
}
|
||||
|
||||
func multiline(errs field.ErrorList) string {
|
||||
if len(errs) == 0 {
|
||||
return "<no errors>"
|
||||
}
|
||||
if len(errs) == 1 {
|
||||
return errs[0].Error()
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for _, err := range errs {
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(err.Error())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ExpectValid validates the value and calls t.Errorf if any validation errors are returned.
|
||||
// Returns ValidationTester to support call chaining.
|
||||
func (v *ValidationTester) ExpectValid() *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
v.T.Run(fmt.Sprintf("%T", v.value), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
errs := v.validate()
|
||||
if len(errs) > 0 {
|
||||
t.Errorf("want no errors, got: %v", multiline(errs))
|
||||
}
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
// ExpectValidAt validates the value and calls t.Errorf for any validation errors at the given path.
|
||||
// Returns ValidationTester to support call chaining.
|
||||
func (v *ValidationTester) ExpectValidAt(fldPath *field.Path) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
v.T.Run(fmt.Sprintf("%T.%v", v.value, fldPath), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var got field.ErrorList
|
||||
for _, e := range v.validate() {
|
||||
if e.Field == fldPath.String() {
|
||||
got = append(got, e)
|
||||
}
|
||||
}
|
||||
if len(got) > 0 {
|
||||
t.Errorf("want no errors at %v, got: %v", fldPath, got)
|
||||
}
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
// ExpectInvalid validates the value and calls t.Errorf if want does not match the actual errors.
|
||||
// Returns ValidationTester to support call chaining.
|
||||
func (v *ValidationTester) ExpectInvalid(want ...*field.Error) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
return v.expectInvalid(byFullError, want...)
|
||||
}
|
||||
|
||||
// ExpectValidateFalse validates the value and calls t.Errorf if the actual errors do not
|
||||
// match the given validateFalseArgs. For example, if the value to validate has a
|
||||
// single `+validateFalse="type T1"` tag, ExpectValidateFalse("type T1") will pass.
|
||||
// Returns ValidationTester to support call chaining.
|
||||
func (v *ValidationTester) ExpectValidateFalse(validateFalseArgs ...string) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
var want []*field.Error
|
||||
for _, s := range validateFalseArgs {
|
||||
want = append(want, field.Invalid(nil, "", fmt.Sprintf("forced failure: %s", s)))
|
||||
}
|
||||
return v.expectInvalid(byDetail, want...)
|
||||
}
|
||||
|
||||
func (v *ValidationTester) ExpectValidateFalseByPath(validateFalseArgsByPath map[string][]string) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
v.T.Run(fmt.Sprintf("%T", v.value), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
byPath := v.ValidateFalseArgsByPath()
|
||||
// ensure args are sorted
|
||||
for _, args := range validateFalseArgsByPath {
|
||||
sort.Strings(args)
|
||||
}
|
||||
if !cmp.Equal(validateFalseArgsByPath, byPath) {
|
||||
t.Errorf("validateFalse args, grouped by field path, differed from expected:\n%s\n", cmp.Diff(validateFalseArgsByPath, byPath, cmpopts.SortMaps(stdcmp.Less[string])))
|
||||
}
|
||||
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *ValidationTester) ValidateFalseArgsByPath() map[string][]string {
|
||||
byPath := map[string][]string{}
|
||||
errs := v.validate()
|
||||
for _, e := range errs {
|
||||
if strings.HasPrefix(e.Detail, "forced failure: ") {
|
||||
arg := strings.TrimPrefix(e.Detail, "forced failure: ")
|
||||
f := e.Field
|
||||
if f == "<nil>" {
|
||||
f = ""
|
||||
}
|
||||
byPath[f] = append(byPath[f], arg)
|
||||
}
|
||||
}
|
||||
// ensure args are sorted
|
||||
for _, args := range byPath {
|
||||
sort.Strings(args)
|
||||
}
|
||||
return byPath
|
||||
}
|
||||
|
||||
func (v *ValidationTester) ExpectRegexpsByPath(regexpStringsByPath map[string][]string) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
v.T.Run(fmt.Sprintf("%T", v.value), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
errorsByPath := v.getErrorsByPath()
|
||||
|
||||
// sanity check
|
||||
if want, got := len(regexpStringsByPath), len(errorsByPath); got != want {
|
||||
t.Fatalf("wrong number of error-fields: expected %d, got %d:\nwanted:\n%sgot:\n%s",
|
||||
want, got, renderByPath(regexpStringsByPath), renderByPath(errorsByPath))
|
||||
}
|
||||
|
||||
// compile regexps
|
||||
regexpsByPath := map[string][]*regexp.Regexp{}
|
||||
for field, strs := range regexpStringsByPath {
|
||||
regexps := make([]*regexp.Regexp, 0, len(strs))
|
||||
for _, str := range strs {
|
||||
regexps = append(regexps, regexp.MustCompile(str))
|
||||
}
|
||||
regexpsByPath[field] = regexps
|
||||
}
|
||||
|
||||
for field := range errorsByPath {
|
||||
errors := errorsByPath[field]
|
||||
regexps := regexpsByPath[field]
|
||||
|
||||
// sanity check
|
||||
if want, got := len(regexps), len(errors); got != want {
|
||||
t.Fatalf("field %q: wrong number of errors: expected %d, got %d:\nwanted:\n%sgot:\n%s",
|
||||
field, want, got, renderList(regexpStringsByPath[field]), renderList(errors))
|
||||
}
|
||||
|
||||
// build a set of errors and expectations, so we can track them,
|
||||
expSet := sets.New(regexps...)
|
||||
|
||||
for _, err := range errors {
|
||||
var found *regexp.Regexp
|
||||
for _, re := range regexps {
|
||||
if re.MatchString(err) {
|
||||
found = re
|
||||
break // done with regexps
|
||||
}
|
||||
}
|
||||
if found != nil {
|
||||
expSet.Delete(found)
|
||||
continue // next error
|
||||
}
|
||||
t.Errorf("field %q, error %q did not match any expectation", field, err)
|
||||
}
|
||||
if len(expSet) != 0 {
|
||||
t.Errorf("field %q had unsatisfied expectations: %q", field, expSet.UnsortedList())
|
||||
}
|
||||
}
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *ValidationTester) getErrorsByPath() map[string][]string {
|
||||
byPath := map[string][]string{}
|
||||
errs := v.validate()
|
||||
for _, e := range errs {
|
||||
f := e.Field
|
||||
if f == "<nil>" {
|
||||
f = ""
|
||||
}
|
||||
byPath[f] = append(byPath[f], e.ErrorBody())
|
||||
}
|
||||
// ensure args are sorted
|
||||
for _, args := range byPath {
|
||||
sort.Strings(args)
|
||||
}
|
||||
return byPath
|
||||
}
|
||||
|
||||
func renderByPath(byPath map[string][]string) string {
|
||||
keys := []string{}
|
||||
for key := range byPath {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, vals := range byPath {
|
||||
sort.Strings(vals)
|
||||
}
|
||||
|
||||
buf := strings.Builder{}
|
||||
for _, key := range keys {
|
||||
vals := byPath[key]
|
||||
for _, val := range vals {
|
||||
buf.WriteString(fmt.Sprintf("\t%s: %q\n", key, val))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func renderList(list []string) string {
|
||||
buf := strings.Builder{}
|
||||
for _, item := range list {
|
||||
buf.WriteString(fmt.Sprintf("\t%q\n", item))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (v *ValidationTester) expectInvalid(matcher matcher, errs ...*field.Error) *ValidationTester {
|
||||
v.T.Helper()
|
||||
|
||||
v.T.Run(fmt.Sprintf("%T", v.value), func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
want := sets.New[string]()
|
||||
for _, e := range errs {
|
||||
want.Insert(matcher(e))
|
||||
}
|
||||
|
||||
got := sets.New[string]()
|
||||
for _, e := range v.validate() {
|
||||
got.Insert(matcher(e))
|
||||
}
|
||||
if !got.Equal(want) {
|
||||
t.Errorf("validation errors differed from expected:\n%v\n", cmp.Diff(want, got, cmpopts.SortMaps(stdcmp.Less[string])))
|
||||
|
||||
for x := range got.Difference(want) {
|
||||
fmt.Printf("%q,\n", strings.TrimPrefix(x, "forced failure: "))
|
||||
}
|
||||
}
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
type matcher func(err *field.Error) string
|
||||
|
||||
func byDetail(err *field.Error) string {
|
||||
return err.Detail
|
||||
}
|
||||
|
||||
func byFullError(err *field.Error) string {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func (v *ValidationTester) validate() field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
if v.oldValue == nil {
|
||||
errs = v.s.Validate(context.Background(), v.opts, v.value)
|
||||
} else {
|
||||
errs = v.s.ValidateUpdate(context.Background(), v.opts, v.value, v.oldValue)
|
||||
}
|
||||
return errs
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// These tags return a fixed pass/fail state.
|
||||
validateTrueTagName = "k8s:validateTrue"
|
||||
validateFalseTagName = "k8s:validateFalse"
|
||||
|
||||
// This tag always returns an error from ExtractValidations.
|
||||
validateErrorTagName = "k8s:validateError"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTagValidator(fixedResultTagValidator{result: true})
|
||||
RegisterTagValidator(fixedResultTagValidator{result: false})
|
||||
RegisterTagValidator(fixedResultTagValidator{error: true})
|
||||
}
|
||||
|
||||
type fixedResultTagValidator struct {
|
||||
result bool
|
||||
error bool
|
||||
}
|
||||
|
||||
func (fixedResultTagValidator) Init(_ Config) {}
|
||||
|
||||
func (frtv fixedResultTagValidator) TagName() string {
|
||||
if frtv.error {
|
||||
return validateErrorTagName
|
||||
} else if frtv.result {
|
||||
return validateTrueTagName
|
||||
}
|
||||
return validateFalseTagName
|
||||
}
|
||||
|
||||
var fixedResultTagValidScopes = sets.New(ScopeAny)
|
||||
|
||||
func (fixedResultTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return fixedResultTagValidScopes
|
||||
}
|
||||
|
||||
func (frtv fixedResultTagValidator) GetValidations(context Context, _ []string, payload string) (Validations, error) {
|
||||
var result Validations
|
||||
|
||||
if frtv.error {
|
||||
return result, fmt.Errorf("forced error: %q", payload)
|
||||
}
|
||||
|
||||
tag, err := frtv.parseTagPayload(payload)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("can't decode tag payload: %w", err)
|
||||
}
|
||||
result.AddFunction(GenericFunction(frtv.TagName(), tag.flags, fixedResultValidator, tag.typeArgs, frtv.result, tag.msg))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var (
|
||||
fixedResultValidator = types.Name{Package: libValidationPkg, Name: "FixedResult"}
|
||||
)
|
||||
|
||||
type fixedResultPayload struct {
|
||||
flags FunctionFlags
|
||||
msg string
|
||||
typeArgs []types.Name
|
||||
}
|
||||
|
||||
func (fixedResultTagValidator) parseTagPayload(in string) (fixedResultPayload, error) {
|
||||
type payload struct {
|
||||
Flags []string `json:"flags"`
|
||||
Msg string `json:"msg"`
|
||||
TypeArg string `json:"typeArg,omitempty"`
|
||||
}
|
||||
// We expect either a string (maybe empty) or a JSON object.
|
||||
if len(in) == 0 {
|
||||
return fixedResultPayload{}, nil
|
||||
}
|
||||
var pl payload
|
||||
if err := json.Unmarshal([]byte(in), &pl); err != nil {
|
||||
s := ""
|
||||
if err := json.Unmarshal([]byte(in), &s); err != nil {
|
||||
return fixedResultPayload{}, fmt.Errorf("error parsing JSON value: %w (%q)", err, in)
|
||||
}
|
||||
return fixedResultPayload{msg: s}, nil
|
||||
}
|
||||
// The msg field is required in JSON mode.
|
||||
if pl.Msg == "" {
|
||||
return fixedResultPayload{}, fmt.Errorf("JSON msg is required")
|
||||
}
|
||||
var flags FunctionFlags
|
||||
for _, fl := range pl.Flags {
|
||||
switch fl {
|
||||
case "ShortCircuit":
|
||||
flags |= ShortCircuit
|
||||
case "NonError":
|
||||
flags |= NonError
|
||||
default:
|
||||
return fixedResultPayload{}, fmt.Errorf("unknown flag: %q", fl)
|
||||
}
|
||||
}
|
||||
var typeArgs []types.Name
|
||||
if tn := pl.TypeArg; len(tn) > 0 {
|
||||
if !strings.HasPrefix(tn, "*") {
|
||||
tn = "*" + tn // We always need the pointer type.
|
||||
}
|
||||
typeArgs = []types.Name{{Package: "", Name: tn}}
|
||||
}
|
||||
|
||||
return fixedResultPayload{flags, pl.Msg, typeArgs}, nil
|
||||
}
|
||||
|
||||
func (frtv fixedResultTagValidator) Docs() TagDoc {
|
||||
doc := TagDoc{
|
||||
Tag: frtv.TagName(),
|
||||
Scopes: frtv.ValidScopes().UnsortedList(),
|
||||
}
|
||||
if frtv.error {
|
||||
doc.Description = "Always fails code generation (useful for testing)."
|
||||
doc.Payloads = []TagPayloadDoc{{
|
||||
Description: "<string>",
|
||||
Docs: "This string will be included in the error message.",
|
||||
}}
|
||||
} else {
|
||||
// True and false have the same payloads.
|
||||
doc.Payloads = []TagPayloadDoc{{
|
||||
Description: "<none>",
|
||||
}, {
|
||||
Description: "<quoted-string>",
|
||||
Docs: "The generated code will include this string.",
|
||||
}, {
|
||||
Description: "<json-object>",
|
||||
Docs: "",
|
||||
Schema: []TagPayloadSchema{{
|
||||
Key: "flags",
|
||||
Value: "<list-of-flag-string>",
|
||||
Docs: `values: ShortCircuit, NonError`,
|
||||
}, {
|
||||
Key: "msg",
|
||||
Value: "<string>",
|
||||
Docs: "The generated code will include this string.",
|
||||
}, {
|
||||
Key: "typeArg",
|
||||
Value: "<string>",
|
||||
Docs: "The type arg in generated code (must be the value-type, not pointer).",
|
||||
}},
|
||||
}}
|
||||
if frtv.result {
|
||||
doc.Description = "Always passes validation (useful for testing)."
|
||||
} else {
|
||||
doc.Description = "Always fails validation (useful for testing)."
|
||||
}
|
||||
}
|
||||
return doc
|
||||
}
|
Loading…
Reference in New Issue
Block a user