mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
Introduce validation-gen
Adds code-generator/cmd/validation-gen. This provides the machinery to discover `//+` tags in types.go files, register plugins to handle the tags, and generate validation code. 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
7f5e1baeee
commit
c1f9e6b8ee
@ -61,6 +61,7 @@
|
||||
- k8s.io/code-generator
|
||||
- k8s.io/kube-openapi
|
||||
- k8s.io/klog
|
||||
- k8s.io/utils/ptr
|
||||
|
||||
- baseImportPath: "./staging/src/k8s.io/component-base"
|
||||
allowedImports:
|
||||
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 operation
|
||||
|
||||
import "k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
// Operation provides contextual information about a validation request and the API
|
||||
// operation being validated.
|
||||
// This type is intended for use with generate validation code and may be enhanced
|
||||
// in the future to include other information needed to validate requests.
|
||||
type Operation struct {
|
||||
// Type is the category of operation being validated. This does not
|
||||
// differentiate between HTTP verbs like PUT and PATCH, but rather merges
|
||||
// those into a single "Update" category.
|
||||
Type Type
|
||||
|
||||
// Options declare the options enabled for validation.
|
||||
//
|
||||
// Options should be set according to a resource validation strategy before validation
|
||||
// is performed, and must be treated as read-only during validation.
|
||||
//
|
||||
// Options are identified by string names. Option string names may match the name of a feature
|
||||
// gate, in which case the presence of the name in the set indicates that the feature is
|
||||
// considered enabled for the resource being validated. Note that a resource may have a
|
||||
// feature enabled even when the feature gate is disabled. This can happen when feature is
|
||||
// already in-use by a resource, often because the feature gate was enabled when the
|
||||
// resource first began using the feature.
|
||||
//
|
||||
// Unset options are disabled/false.
|
||||
Options sets.Set[string]
|
||||
}
|
||||
|
||||
// Code is the request operation to be validated.
|
||||
type Type uint32
|
||||
|
||||
const (
|
||||
// Create indicates the request being validated is for a resource create operation.
|
||||
Create Type = iota
|
||||
|
||||
// Update indicates the request being validated is for a resource update operation.
|
||||
Update
|
||||
)
|
37
staging/src/k8s.io/apimachinery/pkg/api/safe/safe.go
Normal file
37
staging/src/k8s.io/apimachinery/pkg/api/safe/safe.go
Normal file
@ -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 safe
|
||||
|
||||
// Field takes a pointer to any value (which may or may not be nil) and
|
||||
// a function that traverses to a target type R (a typical use case is to dereference a field),
|
||||
// and returns the result of the traversal, or the zero value of the target type.
|
||||
// This is roughly equivalent to "value != nil ? fn(value) : zero-value" in languages that support the ternary operator.
|
||||
func Field[V any, R any](value *V, fn func(*V) R) R {
|
||||
if value == nil {
|
||||
var zero R
|
||||
return zero
|
||||
}
|
||||
o := fn(value)
|
||||
return o
|
||||
}
|
||||
|
||||
// Cast takes any value, attempts to cast it to T, and returns the T value if
|
||||
// the cast is successful, or else the zero value of T.
|
||||
func Cast[T any](value any) T {
|
||||
result, _ := value.(T)
|
||||
return result
|
||||
}
|
28
staging/src/k8s.io/apimachinery/pkg/api/validate/common.go
Normal file
28
staging/src/k8s.io/apimachinery/pkg/api/validate/common.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
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"
|
||||
)
|
||||
|
||||
// ValidateFunc is a function that validates a value, possibly considering the
|
||||
// old value (if any).
|
||||
type ValidateFunc[T any] func(ctx context.Context, op operation.Operation, fldPath *field.Path, newValue, oldValue T) field.ErrorList
|
160
staging/src/k8s.io/code-generator/cmd/validation-gen/lint.go
Normal file
160
staging/src/k8s.io/code-generator/cmd/validation-gen/lint.go
Normal file
@ -0,0 +1,160 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/gengo/v2/types"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// linter is a struct that holds the state of the linting process.
|
||||
// It contains a map of types that have been linted, a list of linting rules,
|
||||
// and a list of errors that occurred during the linting process.
|
||||
type linter struct {
|
||||
linted map[*types.Type]bool
|
||||
rules []lintRule
|
||||
// lintErrors is a list of errors that occurred during the linting process.
|
||||
// lintErrors would be in the format:
|
||||
// field <field_name>: <lint broken message>
|
||||
// type <type_name>: <lint broken message>
|
||||
lintErrors []error
|
||||
}
|
||||
|
||||
var defaultRules = []lintRule{
|
||||
ruleOptionalAndRequired,
|
||||
ruleRequiredAndDefault,
|
||||
}
|
||||
|
||||
func (l *linter) AddError(field, msg string) {
|
||||
l.lintErrors = append(l.lintErrors, fmt.Errorf("%s: %s", field, msg))
|
||||
}
|
||||
|
||||
func newLinter(rules ...lintRule) *linter {
|
||||
if len(rules) == 0 {
|
||||
rules = defaultRules
|
||||
}
|
||||
return &linter{
|
||||
linted: make(map[*types.Type]bool),
|
||||
rules: rules,
|
||||
lintErrors: []error{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *linter) lintType(t *types.Type) error {
|
||||
if _, ok := l.linted[t]; ok {
|
||||
return nil
|
||||
}
|
||||
l.linted[t] = true
|
||||
|
||||
if t.CommentLines != nil {
|
||||
klog.V(5).Infof("linting type %s", t.Name.String())
|
||||
lintErrs, err := l.lintComments(t.CommentLines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, lintErr := range lintErrs {
|
||||
l.AddError("type "+t.Name.String(), lintErr)
|
||||
}
|
||||
}
|
||||
switch t.Kind {
|
||||
case types.Alias:
|
||||
// Recursively lint the underlying type of the alias.
|
||||
if err := l.lintType(t.Underlying); err != nil {
|
||||
return err
|
||||
}
|
||||
case types.Struct:
|
||||
// Recursively lint each member of the struct.
|
||||
for _, member := range t.Members {
|
||||
klog.V(5).Infof("linting comments for field %s of type %s", member.String(), t.Name.String())
|
||||
lintErrs, err := l.lintComments(member.CommentLines)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, lintErr := range lintErrs {
|
||||
l.AddError("type "+t.Name.String(), lintErr)
|
||||
}
|
||||
if err := l.lintType(member.Type); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case types.Slice, types.Array, types.Pointer:
|
||||
// Recursively lint the element type of the slice or array.
|
||||
if err := l.lintType(t.Elem); err != nil {
|
||||
return err
|
||||
}
|
||||
case types.Map:
|
||||
// Recursively lint the key and element types of the map.
|
||||
if err := l.lintType(t.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.lintType(t.Elem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lintRule is a function that validates a slice of comments.
|
||||
// It returns a string as an error message if the comments are invalid,
|
||||
// and an error there is an error happened during the linting process.
|
||||
type lintRule func(comments []string) (string, error)
|
||||
|
||||
// lintComments runs all registered rules on a slice of comments.
|
||||
func (l *linter) lintComments(comments []string) ([]string, error) {
|
||||
var lintErrs []string
|
||||
for _, rule := range l.rules {
|
||||
if msg, err := rule(comments); err != nil {
|
||||
return nil, err
|
||||
} else if msg != "" {
|
||||
lintErrs = append(lintErrs, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return lintErrs, nil
|
||||
}
|
||||
|
||||
// conflictingTagsRule checks for conflicting tags in a slice of comments.
|
||||
func conflictingTagsRule(comments []string, tags ...string) (string, error) {
|
||||
if len(tags) < 2 {
|
||||
return "", fmt.Errorf("at least two tags must be provided")
|
||||
}
|
||||
tagCount := make(map[string]bool)
|
||||
for _, comment := range comments {
|
||||
for _, tag := range tags {
|
||||
if strings.HasPrefix(comment, tag) {
|
||||
tagCount[tag] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tagCount) > 1 {
|
||||
return fmt.Sprintf("conflicting tags: {%s}", strings.Join(tags, ", ")), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// ruleOptionalAndRequired checks for conflicting tags +k8s:optional and +k8s:required in a slice of comments.
|
||||
func ruleOptionalAndRequired(comments []string) (string, error) {
|
||||
return conflictingTagsRule(comments, "+k8s:optional", "+k8s:required")
|
||||
}
|
||||
|
||||
// ruleRequiredAndDefault checks for conflicting tags +k8s:required and +default in a slice of comments.
|
||||
func ruleRequiredAndDefault(comments []string) (string, error) {
|
||||
return conflictingTagsRule(comments, "+k8s:required", "+default")
|
||||
}
|
@ -0,0 +1,412 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
func ruleAlwaysPass(comments []string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func ruleAlwaysFail(comments []string) (string, error) {
|
||||
return "lintfail", nil
|
||||
}
|
||||
|
||||
func ruleAlwaysErr(comments []string) (string, error) {
|
||||
return "", errors.New("linterr")
|
||||
}
|
||||
|
||||
func mkCountRule(counter *int, realRule lintRule) lintRule {
|
||||
return func(comments []string) (string, error) {
|
||||
(*counter)++
|
||||
return realRule(comments)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintCommentsRuleInvocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rules []lintRule
|
||||
commentLineGroups [][]string
|
||||
wantErr bool
|
||||
wantCount int
|
||||
}{
|
||||
{
|
||||
name: "0 rules, 0 comments",
|
||||
rules: []lintRule{},
|
||||
commentLineGroups: [][]string{},
|
||||
wantErr: false,
|
||||
wantCount: 0,
|
||||
},
|
||||
{
|
||||
name: "1 rule, 1 comment",
|
||||
rules: []lintRule{ruleAlwaysPass},
|
||||
commentLineGroups: [][]string{{"comment"}},
|
||||
wantErr: false,
|
||||
wantCount: 1,
|
||||
},
|
||||
{
|
||||
name: "3 rules, 3 comments",
|
||||
rules: []lintRule{ruleAlwaysPass, ruleAlwaysFail, ruleAlwaysErr},
|
||||
commentLineGroups: [][]string{{"comment1"}, {"comment2"}, {"comment3"}},
|
||||
wantErr: true,
|
||||
wantCount: 9,
|
||||
},
|
||||
{
|
||||
name: "1 rule, 1 comment, rule fails",
|
||||
rules: []lintRule{ruleAlwaysFail},
|
||||
commentLineGroups: [][]string{{"comment"}},
|
||||
wantErr: false,
|
||||
wantCount: 1,
|
||||
},
|
||||
{
|
||||
name: "1 rule, 1 comment, rule errors",
|
||||
rules: []lintRule{ruleAlwaysErr},
|
||||
commentLineGroups: [][]string{{"comment"}},
|
||||
wantErr: true,
|
||||
wantCount: 1,
|
||||
},
|
||||
{
|
||||
name: "3 rules, 1 comment, rule errors in the middle",
|
||||
rules: []lintRule{ruleAlwaysPass, ruleAlwaysErr, ruleAlwaysFail},
|
||||
commentLineGroups: [][]string{{"comment"}},
|
||||
wantErr: true,
|
||||
wantCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
counter := 0
|
||||
rules := make([]lintRule, len(tt.rules))
|
||||
for i, rule := range tt.rules {
|
||||
rules[i] = mkCountRule(&counter, rule)
|
||||
}
|
||||
l := newLinter(rules...)
|
||||
for _, commentLines := range tt.commentLineGroups {
|
||||
_, err := l.lintComments(commentLines)
|
||||
gotErr := err != nil
|
||||
if gotErr != tt.wantErr {
|
||||
t.Errorf("lintComments() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
if counter != tt.wantCount {
|
||||
t.Errorf("expected %d rule invocations, got %d", tt.wantCount, counter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleOptionalAndRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
comments []string
|
||||
wantMsg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no comments",
|
||||
comments: []string{},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "only optional",
|
||||
comments: []string{"+k8s:optional"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "only required",
|
||||
comments: []string{"+k8s:required"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "optional and required",
|
||||
comments: []string{"+k8s:optional", "+k8s:required"},
|
||||
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
|
||||
},
|
||||
{
|
||||
name: "optional, empty, required",
|
||||
comments: []string{"+k8s:optional", "", "+k8s:required"},
|
||||
wantMsg: "conflicting tags: {+k8s:optional, +k8s:required}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg, _ := ruleOptionalAndRequired(tt.comments)
|
||||
if msg != tt.wantMsg {
|
||||
t.Errorf("ruleOptionalAndRequired() msg = %v, wantMsg %v", msg, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleRequiredAndDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
comments []string
|
||||
wantMsg string
|
||||
}{
|
||||
{
|
||||
name: "no comments",
|
||||
comments: []string{},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "only required",
|
||||
comments: []string{"+k8s:required"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "only default",
|
||||
comments: []string{"+default=somevalue"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "required and default",
|
||||
comments: []string{"+k8s:required", "+default=somevalue"},
|
||||
wantMsg: "conflicting tags: {+k8s:required, +default}",
|
||||
},
|
||||
{
|
||||
name: "required, empty, default",
|
||||
comments: []string{"+k8s:required", "", "+default=somevalue"},
|
||||
wantMsg: "conflicting tags: {+k8s:required, +default}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg, _ := ruleRequiredAndDefault(tt.comments)
|
||||
if msg != tt.wantMsg {
|
||||
t.Errorf("ruleRequiredAndDefault() msg = %v, wantMsg %v", msg, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictingTagsRule(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
comments []string
|
||||
tags []string
|
||||
wantMsg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no comments",
|
||||
comments: []string{},
|
||||
tags: []string{"+tag1", "+tag2"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "only tag1",
|
||||
comments: []string{"+tag1"},
|
||||
tags: []string{"+tag1", "+tag2"},
|
||||
wantMsg: "",
|
||||
},
|
||||
{
|
||||
name: "tag1, empty, tag2",
|
||||
comments: []string{"+tag1", "", "+tag2"},
|
||||
tags: []string{"+tag1", "+tag2"},
|
||||
wantMsg: "conflicting tags: {+tag1, +tag2}",
|
||||
},
|
||||
{
|
||||
name: "3 tags",
|
||||
comments: []string{"tag1", "+tag2", "+tag3=value"},
|
||||
tags: []string{"+tag1", "+tag2", "+tag3"},
|
||||
wantMsg: "conflicting tags: {+tag1, +tag2, +tag3}",
|
||||
},
|
||||
{
|
||||
name: "less than 2 tags",
|
||||
comments: []string{"+tag1"},
|
||||
tags: []string{"+tag1"},
|
||||
wantMsg: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
msg, err := conflictingTagsRule(tt.comments, tt.tags...)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("conflictingTagsRule() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if msg != tt.wantMsg {
|
||||
t.Errorf("conflictingTagsRule() msg = %v, wantMsg %v", msg, tt.wantMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typeToLint *types.Type
|
||||
wantCount int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "No comments",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestType"},
|
||||
CommentLines: nil,
|
||||
},
|
||||
wantCount: 0,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Valid comments",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestType"},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
wantCount: 1,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Pointer type",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestPointer"},
|
||||
Kind: types.Pointer,
|
||||
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
wantCount: 2,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Slice of pointers",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestSlice"},
|
||||
Kind: types.Slice,
|
||||
Elem: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
|
||||
Kind: types.Pointer,
|
||||
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
wantCount: 3,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Map to pointers",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestMap"},
|
||||
Kind: types.Map,
|
||||
Key: &types.Type{Name: types.Name{Package: "testpkg", Name: "KeyType"}, CommentLines: []string{"+k8s:required"}},
|
||||
Elem: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
|
||||
Kind: types.Pointer,
|
||||
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
wantCount: 4,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Alias to pointers",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestAlias"},
|
||||
Kind: types.Alias,
|
||||
Underlying: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "PointerElem"},
|
||||
Kind: types.Pointer,
|
||||
Elem: &types.Type{Name: types.Name{Package: "testpkg", Name: "ElemType"}, CommentLines: []string{"+k8s:optional"}},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
wantCount: 3,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Struct with members",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestStruct"},
|
||||
Kind: types.Struct,
|
||||
Members: []types.Member{
|
||||
{
|
||||
Name: "Field1",
|
||||
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "FieldType"}},
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
},
|
||||
{
|
||||
Name: "Field2",
|
||||
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "FieldType"}},
|
||||
CommentLines: []string{"+k8s:required"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCount: 2,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Nested types",
|
||||
typeToLint: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "TestStruct"},
|
||||
Kind: types.Struct,
|
||||
Members: []types.Member{
|
||||
{
|
||||
Name: "Field1",
|
||||
Type: &types.Type{
|
||||
Name: types.Name{Package: "testpkg", Name: "NestedStruct"},
|
||||
Kind: types.Struct,
|
||||
CommentLines: []string{"+k8s:optional"},
|
||||
Members: []types.Member{
|
||||
{
|
||||
Name: "NestedField1",
|
||||
Type: &types.Type{Name: types.Name{Package: "testpkg", Name: "NestedFieldType"}},
|
||||
CommentLines: []string{"+k8s:required"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCount: 3,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
counter := 0
|
||||
rules := []lintRule{mkCountRule(&counter, ruleAlwaysPass)}
|
||||
l := newLinter(rules...)
|
||||
if err := l.lintType(tt.typeToLint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotErr := len(l.lintErrors) > 0
|
||||
if gotErr != tt.expectError {
|
||||
t.Errorf("LintType() errors = %v, expectError %v", l.lintErrors, tt.expectError)
|
||||
}
|
||||
if counter != tt.wantCount {
|
||||
t.Errorf("expected %d rule invocations, got %d", tt.wantCount, counter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
159
staging/src/k8s.io/code-generator/cmd/validation-gen/main.go
Normal file
159
staging/src/k8s.io/code-generator/cmd/validation-gen/main.go
Normal file
@ -0,0 +1,159 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// validation-gen is a tool for auto-generating Validation functions.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"k8s.io/code-generator/cmd/validation-gen/validators"
|
||||
"k8s.io/gengo/v2"
|
||||
"k8s.io/gengo/v2/generator"
|
||||
"k8s.io/gengo/v2/namer"
|
||||
"k8s.io/gengo/v2/types"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
klog.InitFlags(nil)
|
||||
args := &Args{}
|
||||
|
||||
args.AddFlags(pflag.CommandLine)
|
||||
if err := flag.Set("logtostderr", "true"); err != nil {
|
||||
klog.Fatalf("Error: %v", err)
|
||||
}
|
||||
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
|
||||
pflag.Parse()
|
||||
|
||||
if err := args.Validate(); err != nil {
|
||||
klog.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
if args.PrintDocs {
|
||||
printDocs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
myTargets := func(context *generator.Context) []generator.Target {
|
||||
return GetTargets(context, args)
|
||||
}
|
||||
|
||||
// Run it.
|
||||
if err := gengo.Execute(
|
||||
NameSystems(),
|
||||
DefaultNameSystem(),
|
||||
myTargets,
|
||||
gengo.StdBuildTag,
|
||||
pflag.Args(),
|
||||
); err != nil {
|
||||
klog.Fatalf("Error: %v", err)
|
||||
}
|
||||
klog.V(2).Info("Completed successfully.")
|
||||
}
|
||||
|
||||
type Args struct {
|
||||
OutputFile string
|
||||
ExtraPkgs []string // Always consider these as last-ditch possibilities for validations.
|
||||
GoHeaderFile string
|
||||
PrintDocs bool
|
||||
Lint bool
|
||||
}
|
||||
|
||||
// AddFlags add the generator flags to the flag set.
|
||||
func (args *Args) AddFlags(fs *pflag.FlagSet) {
|
||||
fs.StringVar(&args.OutputFile, "output-file", "generated.validations.go",
|
||||
"the name of the file to be generated")
|
||||
fs.StringSliceVar(&args.ExtraPkgs, "extra-pkg", args.ExtraPkgs,
|
||||
"the import path of a package whose validation can be used by generated code, but is not being generated for")
|
||||
fs.StringVar(&args.GoHeaderFile, "go-header-file", "",
|
||||
"the path to a file containing boilerplate header text; the string \"YEAR\" will be replaced with the current 4-digit year")
|
||||
fs.BoolVar(&args.PrintDocs, "docs", false,
|
||||
"print documentation for supported declarative validations, and then exit")
|
||||
fs.BoolVar(&args.Lint, "lint", false,
|
||||
"only run linting checks, do not generate code")
|
||||
}
|
||||
|
||||
// Validate checks the given arguments.
|
||||
func (args *Args) Validate() error {
|
||||
if len(args.OutputFile) == 0 {
|
||||
return fmt.Errorf("--output-file must be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printDocs() {
|
||||
// We need a fake context to init the validator plugins.
|
||||
c := &generator.Context{
|
||||
Namers: namer.NameSystems{},
|
||||
Universe: types.Universe{},
|
||||
FileTypes: map[string]generator.FileType{},
|
||||
}
|
||||
|
||||
// Initialize all registered validators.
|
||||
validator := validators.InitGlobalValidator(c)
|
||||
|
||||
docs := validator.Docs()
|
||||
for i := range docs {
|
||||
d := &docs[i]
|
||||
slices.Sort(d.Scopes)
|
||||
if d.Usage == "" {
|
||||
// Try to generate a usage string if none was provided.
|
||||
usage := d.Tag
|
||||
if len(d.Args) > 0 {
|
||||
usage += "("
|
||||
for i := range d.Args {
|
||||
if i > 0 {
|
||||
usage += ", "
|
||||
}
|
||||
usage += d.Args[i].Description
|
||||
}
|
||||
usage += ")"
|
||||
}
|
||||
if len(d.Payloads) > 0 {
|
||||
usage += "="
|
||||
if len(d.Payloads) == 1 {
|
||||
usage += d.Payloads[0].Description
|
||||
} else {
|
||||
usage += "<payload>"
|
||||
}
|
||||
}
|
||||
d.Usage = usage
|
||||
}
|
||||
}
|
||||
slices.SortFunc(docs, func(a, b validators.TagDoc) int {
|
||||
return cmp.Compare(a.Tag, b.Tag)
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(docs); err != nil {
|
||||
klog.Fatalf("failed to marshal docs: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(buf.String())
|
||||
}
|
383
staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go
Normal file
383
staging/src/k8s.io/code-generator/cmd/validation-gen/targets.go
Normal file
@ -0,0 +1,383 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/code-generator/cmd/validation-gen/validators"
|
||||
"k8s.io/gengo/v2"
|
||||
"k8s.io/gengo/v2/generator"
|
||||
"k8s.io/gengo/v2/namer"
|
||||
"k8s.io/gengo/v2/types"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// These are the comment tags that carry parameters for validation generation.
|
||||
const (
|
||||
tagName = "k8s:validation-gen"
|
||||
inputTagName = "k8s:validation-gen-input"
|
||||
schemeRegistryTagName = "k8s:validation-gen-scheme-registry" // defaults to k8s.io/apimachinery/pkg.runtime.Scheme
|
||||
testFixtureTagName = "k8s:validation-gen-test-fixture" // if set, generate go test files for test fixtures. Supported values: "validateFalse".
|
||||
)
|
||||
|
||||
var (
|
||||
runtimePkg = "k8s.io/apimachinery/pkg/runtime"
|
||||
schemeType = types.Name{Package: runtimePkg, Name: "Scheme"}
|
||||
)
|
||||
|
||||
func extractTag(comments []string) ([]string, bool) {
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to extract tags: %v", err)
|
||||
}
|
||||
values, found := tags[tagName]
|
||||
if !found || len(values) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
result := make([]string, len(values))
|
||||
for i, tag := range values {
|
||||
result[i] = tag.Value
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
|
||||
func extractInputTag(comments []string) []string {
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{inputTagName}, comments)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to extract input tags: %v", err)
|
||||
}
|
||||
values, found := tags[inputTagName]
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]string, len(values))
|
||||
for i, tag := range values {
|
||||
result[i] = tag.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func checkTag(comments []string, require ...string) bool {
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{tagName}, comments)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to extract tags: %v", err)
|
||||
}
|
||||
values, found := tags[tagName]
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(require) == 0 {
|
||||
return len(values) == 1 && values[0].Value == ""
|
||||
}
|
||||
|
||||
valueStrings := make([]string, len(values))
|
||||
for i, tag := range values {
|
||||
valueStrings[i] = tag.Value
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(valueStrings, require)
|
||||
}
|
||||
|
||||
func schemeRegistryTag(pkg *types.Package) types.Name {
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{schemeRegistryTagName}, pkg.Comments)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to extract scheme registry tags: %v", err)
|
||||
}
|
||||
values, found := tags[schemeRegistryTagName]
|
||||
if !found || len(values) == 0 {
|
||||
return schemeType // default
|
||||
}
|
||||
if len(values) > 1 {
|
||||
panic(fmt.Sprintf("Package %q contains more than one usage of %q", pkg.Path, schemeRegistryTagName))
|
||||
}
|
||||
return types.ParseFullyQualifiedName(values[0].Value)
|
||||
}
|
||||
|
||||
var testFixtureTagValues = sets.New("validateFalse")
|
||||
|
||||
func testFixtureTag(pkg *types.Package) sets.Set[string] {
|
||||
result := sets.New[string]()
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", []string{testFixtureTagName}, pkg.Comments)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed to extract test fixture tags: %v", err)
|
||||
}
|
||||
values, found := tags[testFixtureTagName]
|
||||
if !found {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, tag := range values {
|
||||
if !testFixtureTagValues.Has(tag.Value) {
|
||||
panic(fmt.Sprintf("Package %q: %s must be one of '%s', but got: %s", pkg.Path, testFixtureTagName, testFixtureTagValues.UnsortedList(), tag.Value))
|
||||
}
|
||||
result.Insert(tag.Value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NameSystems returns the name system used by the generators in this package.
|
||||
func NameSystems() namer.NameSystems {
|
||||
return namer.NameSystems{
|
||||
"public": namer.NewPublicNamer(1),
|
||||
"raw": namer.NewRawNamer("", nil),
|
||||
"objectvalidationfn": validationFnNamer(),
|
||||
"private": namer.NewPrivateNamer(0),
|
||||
"name": namer.NewPublicNamer(0),
|
||||
}
|
||||
}
|
||||
|
||||
func validationFnNamer() *namer.NameStrategy {
|
||||
return &namer.NameStrategy{
|
||||
Prefix: "Validate_",
|
||||
Join: func(pre string, in []string, post string) string {
|
||||
return pre + strings.Join(in, "_") + post
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultNameSystem returns the default name system for ordering the types to be
|
||||
// processed by the generators in this package.
|
||||
func DefaultNameSystem() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
func GetTargets(context *generator.Context, args *Args) []generator.Target {
|
||||
boilerplate, err := gengo.GoBoilerplate(args.GoHeaderFile, gengo.StdBuildTag, gengo.StdGeneratedBy)
|
||||
if err != nil {
|
||||
klog.Fatalf("Failed loading boilerplate: %v", err)
|
||||
}
|
||||
|
||||
var targets []generator.Target
|
||||
var lintErrs []error
|
||||
|
||||
// First load other "input" packages. We do this as a single call because
|
||||
// it is MUCH faster.
|
||||
inputPkgs := make([]string, 0, len(context.Inputs))
|
||||
pkgToInput := map[string]string{}
|
||||
for _, input := range context.Inputs {
|
||||
klog.V(5).Infof("considering pkg %q", input)
|
||||
|
||||
pkg := context.Universe[input]
|
||||
|
||||
// if the types are not in the same package where the validation
|
||||
// functions are to be emitted
|
||||
inputTags := extractInputTag(pkg.Comments)
|
||||
if len(inputTags) > 1 {
|
||||
panic(fmt.Sprintf("there may only be one input tag, got %#v", inputTags))
|
||||
}
|
||||
if len(inputTags) == 1 {
|
||||
inputPath := inputTags[0]
|
||||
if strings.HasPrefix(inputPath, "./") || strings.HasPrefix(inputPath, "../") {
|
||||
// this is a relative dir, which will not work under gomodules.
|
||||
// join with the local package path, but warn
|
||||
klog.Fatalf("relative path (%s=%s) is not supported; use full package path (as used by 'import') instead", inputTagName, inputPath)
|
||||
}
|
||||
|
||||
klog.V(5).Infof(" input pkg %v", inputPath)
|
||||
inputPkgs = append(inputPkgs, inputPath)
|
||||
pkgToInput[input] = inputPath
|
||||
} else {
|
||||
pkgToInput[input] = input
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure explicit extra-packages are added.
|
||||
var extraPkgs []string
|
||||
for _, pkg := range args.ExtraPkgs {
|
||||
// In case someone specifies an extra as a path into vendor, convert
|
||||
// it to its "real" package path.
|
||||
if i := strings.Index(pkg, "/vendor/"); i != -1 {
|
||||
pkg = pkg[i+len("/vendor/"):]
|
||||
}
|
||||
extraPkgs = append(extraPkgs, pkg)
|
||||
}
|
||||
if expanded, err := context.FindPackages(extraPkgs...); err != nil {
|
||||
klog.Fatalf("cannot find extra packages: %v", err)
|
||||
} else {
|
||||
extraPkgs = expanded // now in fully canonical form
|
||||
}
|
||||
for _, extra := range extraPkgs {
|
||||
inputPkgs = append(inputPkgs, extra)
|
||||
pkgToInput[extra] = extra
|
||||
}
|
||||
|
||||
// We also need the to be able to look up the packages of inputs
|
||||
inputToPkg := make(map[string]string, len(pkgToInput))
|
||||
for k, v := range pkgToInput {
|
||||
inputToPkg[v] = k
|
||||
}
|
||||
|
||||
if len(inputPkgs) > 0 {
|
||||
if _, err := context.LoadPackages(inputPkgs...); err != nil {
|
||||
klog.Fatalf("cannot load packages: %v", err)
|
||||
}
|
||||
}
|
||||
// update context.Order to the latest context.Universe
|
||||
orderer := namer.Orderer{Namer: namer.NewPublicNamer(1)}
|
||||
context.Order = orderer.OrderUniverse(context.Universe)
|
||||
|
||||
// Initialize all validator plugins exactly once.
|
||||
validator := validators.InitGlobalValidator(context)
|
||||
|
||||
// Build a cache of type->callNode for every type we need.
|
||||
for _, input := range context.Inputs {
|
||||
klog.V(2).InfoS("processing", "pkg", input)
|
||||
|
||||
pkg := context.Universe[input]
|
||||
|
||||
schemeRegistry := schemeRegistryTag(pkg)
|
||||
|
||||
typesWith, found := extractTag(pkg.Comments)
|
||||
if !found {
|
||||
klog.V(2).InfoS(" did not find required tag", "tag", tagName)
|
||||
continue
|
||||
}
|
||||
if len(typesWith) == 1 && typesWith[0] == "" {
|
||||
klog.Fatalf("found package tag %q with no value", tagName)
|
||||
}
|
||||
shouldCreateObjectValidationFn := func(t *types.Type) bool {
|
||||
// opt-out
|
||||
if checkTag(t.SecondClosestCommentLines, "false") {
|
||||
return false
|
||||
}
|
||||
// opt-in
|
||||
if checkTag(t.SecondClosestCommentLines, "true") {
|
||||
return true
|
||||
}
|
||||
// all types
|
||||
for _, v := range typesWith {
|
||||
if v == "*" && !namer.IsPrivateGoName(t.Name.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// For every k8s:validation-gen tag at the package level, interpret the value as a
|
||||
// field name (like TypeMeta, ListMeta, ObjectMeta) and trigger validation generation
|
||||
// for any type with any of the matching field names. Provides a more useful package
|
||||
// level validation than global (because we only need validations on a subset of objects -
|
||||
// usually those with TypeMeta).
|
||||
return isTypeWith(t, typesWith)
|
||||
}
|
||||
|
||||
// Find the right input pkg, which might not be this one.
|
||||
inputPath := pkgToInput[input]
|
||||
// typesPkg is where the types that need validation are defined.
|
||||
// Sometimes it is different from pkg. For example, kubernetes core/v1
|
||||
// types are defined in k8s.io/api/core/v1, while the pkg which holds
|
||||
// defaulter code is at k/k/pkg/api/v1.
|
||||
typesPkg := context.Universe[inputPath]
|
||||
|
||||
// Figure out which types we should be considering further.
|
||||
var rootTypes []*types.Type
|
||||
for _, t := range typesPkg.Types {
|
||||
if shouldCreateObjectValidationFn(t) {
|
||||
rootTypes = append(rootTypes, t)
|
||||
} else {
|
||||
klog.V(6).InfoS("skipping type", "type", t)
|
||||
}
|
||||
}
|
||||
// Deterministic ordering helps in logs and debugging.
|
||||
slices.SortFunc(rootTypes, func(a, b *types.Type) int {
|
||||
return cmp.Compare(a.Name.String(), b.Name.String())
|
||||
})
|
||||
|
||||
td := NewTypeDiscoverer(validator, inputToPkg)
|
||||
for _, t := range rootTypes {
|
||||
klog.V(4).InfoS("pre-processing", "type", t)
|
||||
if err := td.DiscoverType(t); err != nil {
|
||||
klog.Fatalf("failed to generate validations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
l := newLinter()
|
||||
for _, t := range rootTypes {
|
||||
klog.V(4).InfoS("linting root-type", "type", t)
|
||||
if err := l.lintType(t); err != nil {
|
||||
klog.Fatalf("failed to lint type %q: %v", t.Name, err)
|
||||
}
|
||||
if len(l.lintErrors) > 0 {
|
||||
lintErrs = append(lintErrs, l.lintErrors...)
|
||||
}
|
||||
}
|
||||
if args.Lint {
|
||||
klog.V(4).Info("Lint is set, skip appending targets")
|
||||
continue
|
||||
}
|
||||
|
||||
targets = append(targets,
|
||||
&generator.SimpleTarget{
|
||||
PkgName: pkg.Name,
|
||||
PkgPath: pkg.Path,
|
||||
PkgDir: pkg.Dir, // output pkg is the same as the input
|
||||
HeaderComment: boilerplate,
|
||||
|
||||
FilterFunc: func(c *generator.Context, t *types.Type) bool {
|
||||
return t.Name.Package == typesPkg.Path
|
||||
},
|
||||
|
||||
GeneratorsFunc: func(c *generator.Context) (generators []generator.Generator) {
|
||||
generators = []generator.Generator{
|
||||
NewGenValidations(args.OutputFile, pkg.Path, rootTypes, td, inputToPkg, schemeRegistry),
|
||||
}
|
||||
testFixtureTags := testFixtureTag(pkg)
|
||||
if testFixtureTags.Len() > 0 {
|
||||
if !strings.HasSuffix(args.OutputFile, ".go") {
|
||||
panic(fmt.Sprintf("%s requires that output file have .go suffix", testFixtureTagName))
|
||||
}
|
||||
filename := args.OutputFile[0:len(args.OutputFile)-3] + "_test.go"
|
||||
generators = append(generators, FixtureTests(filename, testFixtureTags))
|
||||
}
|
||||
return generators
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if len(lintErrs) > 0 {
|
||||
var lintErrsStr string
|
||||
for _, err := range lintErrs {
|
||||
lintErrsStr += fmt.Sprintf("\n%s", err.Error())
|
||||
}
|
||||
if args.Lint {
|
||||
klog.Fatalf("failed to lint comments: %s", lintErrsStr)
|
||||
} else {
|
||||
klog.Warningf("failed to lint comments: %s", lintErrsStr)
|
||||
}
|
||||
|
||||
}
|
||||
return targets
|
||||
}
|
||||
|
||||
func isTypeWith(t *types.Type, typesWith []string) bool {
|
||||
if t.Kind == types.Struct && len(typesWith) > 0 {
|
||||
for _, field := range t.Members {
|
||||
for _, s := range typesWith {
|
||||
if field.Name == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
1448
staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go
Normal file
1448
staging/src/k8s.io/code-generator/cmd/validation-gen/validation.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,374 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
func TestGetLeafTypeAndPrefixes(t *testing.T) {
|
||||
stringType := &types.Type{
|
||||
Name: types.Name{
|
||||
Package: "",
|
||||
Name: "string",
|
||||
},
|
||||
Kind: types.Builtin,
|
||||
}
|
||||
|
||||
ptrTo := func(t *types.Type) *types.Type {
|
||||
return &types.Type{
|
||||
Name: types.Name{
|
||||
Package: "",
|
||||
Name: "*" + t.Name.String(),
|
||||
},
|
||||
Kind: types.Pointer,
|
||||
Elem: t,
|
||||
}
|
||||
}
|
||||
|
||||
sliceOf := func(t *types.Type) *types.Type {
|
||||
return &types.Type{
|
||||
Name: types.Name{
|
||||
Package: "",
|
||||
Name: "[]" + t.Name.String(),
|
||||
},
|
||||
Kind: types.Slice,
|
||||
Elem: t,
|
||||
}
|
||||
}
|
||||
|
||||
mapOf := func(t *types.Type) *types.Type {
|
||||
return &types.Type{
|
||||
Name: types.Name{
|
||||
Package: "",
|
||||
Name: "map[string]" + t.Name.String(),
|
||||
},
|
||||
Kind: types.Map,
|
||||
Key: stringType,
|
||||
Elem: t,
|
||||
}
|
||||
}
|
||||
|
||||
aliasOf := func(name string, t *types.Type) *types.Type {
|
||||
return &types.Type{
|
||||
Name: types.Name{
|
||||
Package: "",
|
||||
Name: "Alias_" + name,
|
||||
},
|
||||
Kind: types.Alias,
|
||||
Underlying: t,
|
||||
}
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
in *types.Type
|
||||
expectedType *types.Type
|
||||
expectedTypePfx string
|
||||
expectedExprPfx string
|
||||
}{{
|
||||
// string
|
||||
in: stringType,
|
||||
expectedType: stringType,
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "&",
|
||||
}, {
|
||||
// *string
|
||||
in: ptrTo(stringType),
|
||||
expectedType: stringType,
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// **string
|
||||
in: ptrTo(ptrTo(stringType)),
|
||||
expectedType: stringType,
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// ***string
|
||||
in: ptrTo(ptrTo(ptrTo(stringType))),
|
||||
expectedType: stringType,
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// []string
|
||||
in: sliceOf(stringType),
|
||||
expectedType: sliceOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *[]string
|
||||
in: ptrTo(sliceOf(stringType)),
|
||||
expectedType: sliceOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **[]string
|
||||
in: ptrTo(ptrTo(sliceOf(stringType))),
|
||||
expectedType: sliceOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***[]string
|
||||
in: ptrTo(ptrTo(ptrTo(sliceOf(stringType)))),
|
||||
expectedType: sliceOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// map[string]string
|
||||
in: mapOf(stringType),
|
||||
expectedType: mapOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *map[string]string
|
||||
in: ptrTo(mapOf(stringType)),
|
||||
expectedType: mapOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **map[string]string
|
||||
in: ptrTo(ptrTo(mapOf(stringType))),
|
||||
expectedType: mapOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***map[string]string
|
||||
in: ptrTo(ptrTo(ptrTo(mapOf(stringType)))),
|
||||
expectedType: mapOf(stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// alias of string
|
||||
in: aliasOf("s", stringType),
|
||||
expectedType: aliasOf("s", stringType),
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "&",
|
||||
}, {
|
||||
// alias of *string
|
||||
in: aliasOf("ps", ptrTo(stringType)),
|
||||
expectedType: aliasOf("ps", stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of **string
|
||||
in: aliasOf("pps", ptrTo(ptrTo(stringType))),
|
||||
expectedType: aliasOf("pps", stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of ***string
|
||||
in: aliasOf("ppps", ptrTo(ptrTo(ptrTo(stringType)))),
|
||||
expectedType: aliasOf("ppps", stringType),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of []string
|
||||
in: aliasOf("ls", sliceOf(stringType)),
|
||||
expectedType: aliasOf("ls", sliceOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of *[]string
|
||||
in: aliasOf("pls", ptrTo(sliceOf(stringType))),
|
||||
expectedType: aliasOf("pls", sliceOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of **[]string
|
||||
in: aliasOf("ppls", ptrTo(ptrTo(sliceOf(stringType)))),
|
||||
expectedType: aliasOf("ppls", sliceOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of ***[]string
|
||||
in: aliasOf("pppls", ptrTo(ptrTo(ptrTo(sliceOf(stringType))))),
|
||||
expectedType: aliasOf("pppls", sliceOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of map[string]string
|
||||
in: aliasOf("ms", mapOf(stringType)),
|
||||
expectedType: aliasOf("ms", mapOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of *map[string]string
|
||||
in: aliasOf("pms", ptrTo(mapOf(stringType))),
|
||||
expectedType: aliasOf("pms", mapOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of **map[string]string
|
||||
in: aliasOf("ppms", ptrTo(ptrTo(mapOf(stringType)))),
|
||||
expectedType: aliasOf("ppms", mapOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// alias of ***map[string]string
|
||||
in: aliasOf("pppms", ptrTo(ptrTo(ptrTo(mapOf(stringType))))),
|
||||
expectedType: aliasOf("pppms", mapOf(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *alias-of-string
|
||||
in: ptrTo(aliasOf("s", stringType)),
|
||||
expectedType: aliasOf("s", stringType),
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// **alias-of-string
|
||||
in: ptrTo(ptrTo(aliasOf("s", stringType))),
|
||||
expectedType: aliasOf("s", stringType),
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// ***alias-of-string
|
||||
in: ptrTo(ptrTo(ptrTo(aliasOf("s", stringType)))),
|
||||
expectedType: aliasOf("s", stringType),
|
||||
expectedTypePfx: "*",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// []alias-of-string
|
||||
in: sliceOf(aliasOf("s", stringType)),
|
||||
expectedType: sliceOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *[]alias-of-string
|
||||
in: ptrTo(sliceOf(aliasOf("s", stringType))),
|
||||
expectedType: sliceOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **[]alias-of-string
|
||||
in: ptrTo(ptrTo(sliceOf(aliasOf("s", stringType)))),
|
||||
expectedType: sliceOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***[]alias-of-string
|
||||
in: ptrTo(ptrTo(ptrTo(sliceOf(aliasOf("s", stringType))))),
|
||||
expectedType: sliceOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// map[string]alias-of-string
|
||||
in: mapOf(aliasOf("s", stringType)),
|
||||
expectedType: mapOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *map[string]alias-of-string
|
||||
in: ptrTo(mapOf(aliasOf("s", stringType))),
|
||||
expectedType: mapOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **map[string]alias-of-string
|
||||
in: ptrTo(ptrTo(mapOf(aliasOf("s", stringType)))),
|
||||
expectedType: mapOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***map[string]alias-of-string
|
||||
in: ptrTo(ptrTo(ptrTo(mapOf(aliasOf("s", stringType))))),
|
||||
expectedType: mapOf(aliasOf("s", stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// *alias-of-*string
|
||||
in: ptrTo(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedType: aliasOf("ps", ptrTo(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **alias-of-*string
|
||||
in: ptrTo(ptrTo(aliasOf("ps", ptrTo(stringType)))),
|
||||
expectedType: aliasOf("ps", ptrTo(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***alias-of-*string
|
||||
in: ptrTo(ptrTo(ptrTo(aliasOf("ps", ptrTo(stringType))))),
|
||||
expectedType: aliasOf("ps", ptrTo(stringType)),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// []alias-of-*string
|
||||
in: sliceOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *[]alias-of-*string
|
||||
in: ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType)))),
|
||||
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **[]alias-of-*string
|
||||
in: ptrTo(ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType))))),
|
||||
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***[]alias-of-*string
|
||||
in: ptrTo(ptrTo(ptrTo(sliceOf(aliasOf("ps", ptrTo(stringType)))))),
|
||||
expectedType: sliceOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}, {
|
||||
// map[string]alias-of-*string
|
||||
in: mapOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "",
|
||||
}, {
|
||||
// *map[string]alias-of-*string
|
||||
in: ptrTo(mapOf(aliasOf("ps", ptrTo(stringType)))),
|
||||
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "*",
|
||||
}, {
|
||||
// **map[string]alias-of-*string
|
||||
in: ptrTo(ptrTo(mapOf(aliasOf("ps", ptrTo(stringType))))),
|
||||
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "**",
|
||||
}, {
|
||||
// ***map[string]alias-of-*string
|
||||
in: ptrTo(ptrTo(ptrTo(mapOf(aliasOf("ps", ptrTo(stringType)))))),
|
||||
expectedType: mapOf(aliasOf("ps", ptrTo(stringType))),
|
||||
expectedTypePfx: "",
|
||||
expectedExprPfx: "***",
|
||||
}}
|
||||
|
||||
for _, tc := range cases {
|
||||
leafType, typePfx, exprPfx := getLeafTypeAndPrefixes(tc.in)
|
||||
if got, want := leafType.Name.String(), tc.expectedType.Name.String(); got != want {
|
||||
t.Errorf("%q: wrong leaf type: expected %q, got %q", tc.in, want, got)
|
||||
}
|
||||
if got, want := typePfx, tc.expectedTypePfx; got != want {
|
||||
t.Errorf("%q: wrong type prefix: expected %q, got %q", tc.in, want, got)
|
||||
}
|
||||
if got, want := exprPfx, tc.expectedExprPfx; got != want {
|
||||
t.Errorf("%q: wrong expr prefix: expected %q, got %q", tc.in, want, got)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 (
|
||||
"k8s.io/gengo/v2/parser/tags"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// libValidationPkg is the pkgpath to our "standard library" of validation
|
||||
// functions.
|
||||
libValidationPkg = "k8s.io/apimachinery/pkg/api/validate"
|
||||
)
|
||||
|
||||
func getMemberByJSON(t *types.Type, jsonName string) *types.Member {
|
||||
for i := range t.Members {
|
||||
if jsonTag, ok := tags.LookupJSON(t.Members[i]); ok {
|
||||
if jsonTag.Name == jsonName {
|
||||
return &t.Members[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNilableType returns true if the argument type can be compared to nil.
|
||||
func isNilableType(t *types.Type) bool {
|
||||
for t.Kind == types.Alias {
|
||||
t = t.Underlying
|
||||
}
|
||||
switch t.Kind {
|
||||
case types.Pointer, types.Map, types.Slice, types.Interface: // Note: Arrays are not nilable
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
/*
|
||||
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 (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"k8s.io/gengo/v2"
|
||||
"k8s.io/gengo/v2/generator"
|
||||
)
|
||||
|
||||
// This is the global registry of tag validators. For simplicity this is in
|
||||
// the same package as the implementations, but it should not be used directly.
|
||||
var globalRegistry = ®istry{
|
||||
tagValidators: map[string]TagValidator{},
|
||||
}
|
||||
|
||||
// registry holds a list of registered tags.
|
||||
type registry struct {
|
||||
lock sync.Mutex
|
||||
initialized atomic.Bool // init() was called
|
||||
|
||||
tagValidators map[string]TagValidator // keyed by tagname
|
||||
tagIndex []string // all tag names
|
||||
|
||||
typeValidators []TypeValidator
|
||||
}
|
||||
|
||||
func (reg *registry) addTagValidator(tv TagValidator) {
|
||||
if reg.initialized.Load() {
|
||||
panic("registry was modified after init")
|
||||
}
|
||||
|
||||
reg.lock.Lock()
|
||||
defer reg.lock.Unlock()
|
||||
|
||||
name := tv.TagName()
|
||||
if _, exists := globalRegistry.tagValidators[name]; exists {
|
||||
panic(fmt.Sprintf("tag %q was registered twice", name))
|
||||
}
|
||||
globalRegistry.tagValidators[name] = tv
|
||||
}
|
||||
|
||||
func (reg *registry) addTypeValidator(tv TypeValidator) {
|
||||
if reg.initialized.Load() {
|
||||
panic("registry was modified after init")
|
||||
}
|
||||
|
||||
reg.lock.Lock()
|
||||
defer reg.lock.Unlock()
|
||||
|
||||
globalRegistry.typeValidators = append(globalRegistry.typeValidators, tv)
|
||||
}
|
||||
|
||||
func (reg *registry) init(c *generator.Context) {
|
||||
if reg.initialized.Load() {
|
||||
panic("registry.init() was called twice")
|
||||
}
|
||||
|
||||
reg.lock.Lock()
|
||||
defer reg.lock.Unlock()
|
||||
|
||||
cfg := Config{
|
||||
GengoContext: c,
|
||||
Validator: reg,
|
||||
}
|
||||
|
||||
for _, tv := range globalRegistry.tagValidators {
|
||||
reg.tagIndex = append(reg.tagIndex, tv.TagName())
|
||||
tv.Init(cfg)
|
||||
}
|
||||
sort.Strings(reg.tagIndex)
|
||||
|
||||
for _, tv := range reg.typeValidators {
|
||||
tv.Init(cfg)
|
||||
}
|
||||
slices.SortFunc(reg.typeValidators, func(a, b TypeValidator) int {
|
||||
return cmp.Compare(a.Name(), b.Name())
|
||||
})
|
||||
|
||||
reg.initialized.Store(true)
|
||||
}
|
||||
|
||||
// ExtractValidations considers the given context (e.g. a type definition) and
|
||||
// evaluates registered validators. This includes type validators (which run
|
||||
// against all types) and tag validators which run only if a specific tag is
|
||||
// found in the associated comment block. Any matching validators produce zero
|
||||
// or more validations, which will later be rendered by the code-generation
|
||||
// logic.
|
||||
func (reg *registry) ExtractValidations(context Context, comments []string) (Validations, error) {
|
||||
if !reg.initialized.Load() {
|
||||
panic("registry.init() was not called")
|
||||
}
|
||||
|
||||
validations := Validations{}
|
||||
|
||||
// Extract tags and run matching tag-validators first.
|
||||
tags, err := gengo.ExtractFunctionStyleCommentTags("+", reg.tagIndex, comments)
|
||||
if err != nil {
|
||||
return Validations{}, fmt.Errorf("failed to parse tags: %w", err)
|
||||
}
|
||||
phases := reg.sortTagsIntoPhases(tags)
|
||||
for _, idx := range phases {
|
||||
for _, tag := range idx {
|
||||
vals := tags[tag]
|
||||
tv := reg.tagValidators[tag]
|
||||
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) && !scopes.Has(ScopeAny) {
|
||||
return Validations{}, fmt.Errorf("tag %q cannot be specified on %s", tv.TagName(), context.Scope)
|
||||
}
|
||||
for _, val := range vals { // tags may have multiple values
|
||||
if theseValidations, err := tv.GetValidations(context, val.Args, val.Value); err != nil {
|
||||
return Validations{}, fmt.Errorf("tag %q: %w", tv.TagName(), err)
|
||||
} else {
|
||||
validations.Add(theseValidations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run type-validators after tag validators are done.
|
||||
if context.Scope == ScopeType {
|
||||
// Run all type-validators.
|
||||
for _, tv := range reg.typeValidators {
|
||||
if theseValidations, err := tv.GetValidations(context); err != nil {
|
||||
return Validations{}, fmt.Errorf("type validator %q: %w", tv.Name(), err)
|
||||
} else {
|
||||
validations.Add(theseValidations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validations, nil
|
||||
}
|
||||
|
||||
func (reg *registry) sortTagsIntoPhases(tags map[string][]gengo.Tag) [][]string {
|
||||
// First sort all tags by their name, so the final output is deterministic.
|
||||
//
|
||||
// It makes more sense to sort here, rather than when emitting because:
|
||||
//
|
||||
// Consider a type or field with the following comments:
|
||||
//
|
||||
// // +k8s:validateFalse="111"
|
||||
// // +k8s:validateFalse="222"
|
||||
// // +k8s:ifOptionEnabled(Foo)=+k8s:validateFalse="333"
|
||||
//
|
||||
// Tag extraction will retain the relative order between 111 and 222, but
|
||||
// 333 is extracted as tag "k8s:ifOptionEnabled". Those are all in a map,
|
||||
// which we iterate (in a random order). When it reaches the emit stage,
|
||||
// the "ifOptionEnabled" part is gone, and we will have 3 functionGen
|
||||
// objects, all with tag "k8s:validateFalse", in a non-deterministic order
|
||||
// because of the map iteration. If we sort them at that point, we won't
|
||||
// have enough information to do something smart, unless we look at the
|
||||
// args, which are opaque to us.
|
||||
//
|
||||
// Sorting it earlier means we can sort "k8s:ifOptionEnabled" against
|
||||
// "k8s:validateFalse". All of the records within each of those is
|
||||
// relatively ordered, so the result here would be to put "ifOptionEnabled"
|
||||
// before "validateFalse" (lexicographical is better than random).
|
||||
sortedTags := []string{}
|
||||
for tag := range tags {
|
||||
sortedTags = append(sortedTags, tag)
|
||||
}
|
||||
sort.Strings(sortedTags)
|
||||
|
||||
// Now split them into phases.
|
||||
phase0 := []string{} // regular tags
|
||||
phase1 := []string{} // "late" tags
|
||||
for _, tn := range sortedTags {
|
||||
tv := reg.tagValidators[tn]
|
||||
if _, ok := tv.(LateTagValidator); ok {
|
||||
phase1 = append(phase1, tn)
|
||||
} else {
|
||||
phase0 = append(phase0, tn)
|
||||
}
|
||||
}
|
||||
return [][]string{phase0, phase1}
|
||||
}
|
||||
|
||||
// Docs returns documentation for each tag in this registry.
|
||||
func (reg *registry) Docs() []TagDoc {
|
||||
var result []TagDoc
|
||||
for _, k := range reg.tagIndex {
|
||||
v := reg.tagValidators[k]
|
||||
result = append(result, v.Docs())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RegisterTagValidator must be called by any validator which wants to run when
|
||||
// a specific tag is found.
|
||||
func RegisterTagValidator(tv TagValidator) {
|
||||
globalRegistry.addTagValidator(tv)
|
||||
}
|
||||
|
||||
// RegisterTypeValidator must be called by any validator which wants to run
|
||||
// against every type definition.
|
||||
func RegisterTypeValidator(tv TypeValidator) {
|
||||
globalRegistry.addTypeValidator(tv)
|
||||
}
|
||||
|
||||
// Validator represents an aggregation of validator plugins.
|
||||
type Validator interface {
|
||||
// ExtractValidations considers the given context (e.g. a type definition) and
|
||||
// evaluates registered validators. This includes type validators (which run
|
||||
// against all types) and tag validators which run only if a specific tag is
|
||||
// found in the associated comment block. Any matching validators produce zero
|
||||
// or more validations, which will later be rendered by the code-generation
|
||||
// logic.
|
||||
ExtractValidations(context Context, comments []string) (Validations, error)
|
||||
|
||||
// Docs returns documentation for each known tag.
|
||||
Docs() []TagDoc
|
||||
}
|
||||
|
||||
// InitGlobalValidator must be called exactly once by the main application to
|
||||
// initialize and safely access the global tag registry. Once this is called,
|
||||
// no more validators may be registered.
|
||||
func InitGlobalValidator(c *generator.Context) Validator {
|
||||
globalRegistry.init(c)
|
||||
return globalRegistry
|
||||
}
|
@ -0,0 +1,443 @@
|
||||
/*
|
||||
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 (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/gengo/v2/generator"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
// TagValidator describes a single validation tag and how to use it.
|
||||
type TagValidator interface {
|
||||
// Init initializes the implementation. This will be called exactly once.
|
||||
Init(cfg Config)
|
||||
|
||||
// TagName returns the full tag name (without the "marker" prefix) for this
|
||||
// tag.
|
||||
TagName() string
|
||||
|
||||
// ValidScopes returns the set of scopes where this tag may be used.
|
||||
ValidScopes() sets.Set[Scope]
|
||||
|
||||
// GetValidations returns any validations described by this tag.
|
||||
GetValidations(context Context, args []string, payload string) (Validations, error)
|
||||
|
||||
// Docs returns user-facing documentation for this tag.
|
||||
Docs() TagDoc
|
||||
}
|
||||
|
||||
// LateTagValidator is an optional extension to TagValidator. Any TagValidator
|
||||
// which implements this interface will be evaluated after all TagValidators
|
||||
// which do not.
|
||||
type LateTagValidator interface {
|
||||
LateTagValidator()
|
||||
}
|
||||
|
||||
// TypeValidator describes a validator which runs on every type definition.
|
||||
type TypeValidator interface {
|
||||
// Init initializes the implementation. This will be called exactly once.
|
||||
Init(cfg Config)
|
||||
|
||||
// Name returns a unique name for this validator. This is used for sorting
|
||||
// and logging.
|
||||
Name() string
|
||||
|
||||
// GetValidations returns any validations imposed by this validator for the
|
||||
// given context.
|
||||
//
|
||||
// The way gengo handles type definitions varies between structs and other
|
||||
// types. For struct definitions (e.g. `type Foo struct {}`), the realType
|
||||
// is the struct itself (the Kind field will be `types.Struct`) and the
|
||||
// parentType will be nil. For other types (e.g. `type Bar string`), the
|
||||
// realType will be the underlying type and the parentType will be the
|
||||
// newly defined type (the Kind field will be `types.Alias`).
|
||||
GetValidations(context Context) (Validations, error)
|
||||
}
|
||||
|
||||
// Config carries optional configuration information for use by validators.
|
||||
type Config struct {
|
||||
// GengoContext provides gengo's generator Context. This allows validators
|
||||
// to look up all sorts of other information.
|
||||
GengoContext *generator.Context
|
||||
|
||||
// Validator provides a way to compose validations.
|
||||
//
|
||||
// For example, it is possible to define a validation such as
|
||||
// "+myValidator=+format=IP" by using the registry to extract the
|
||||
// validation for the embedded "+format=IP" and use those to
|
||||
// create the final Validations returned by the "+myValidator" tag.
|
||||
//
|
||||
// This field MUST NOT be used during init, since other validators may not
|
||||
// be initialized yet.
|
||||
Validator Validator
|
||||
}
|
||||
|
||||
// Scope describes where a validation (or potential validation) is located.
|
||||
type Scope string
|
||||
|
||||
// Note: All of these values should be strings which can be used in an error
|
||||
// message such as "may not be used in %s".
|
||||
const (
|
||||
// ScopeAny indicates that a validator may be use in any context. This value
|
||||
// should never appear in a Context struct, since that indicates a
|
||||
// specific use.
|
||||
ScopeAny Scope = "anywhere"
|
||||
|
||||
// ScopeType indicates a validation on a type definition, which applies to
|
||||
// all instances of that type.
|
||||
ScopeType Scope = "type definitions"
|
||||
|
||||
// ScopeField indicates a validation on a particular struct field, which
|
||||
// applies only to that field of that struct.
|
||||
ScopeField Scope = "struct fields"
|
||||
|
||||
// ScopeListVal indicates a validation which applies to all elements of a
|
||||
// list field or type.
|
||||
ScopeListVal Scope = "list values"
|
||||
|
||||
// ScopeMapKey indicates a validation which applies to all keys of a map
|
||||
// field or type.
|
||||
ScopeMapKey Scope = "map keys"
|
||||
|
||||
// ScopeMapVal indicates a validation which applies to all values of a map
|
||||
// field or type.
|
||||
ScopeMapVal Scope = "map values"
|
||||
|
||||
// TODO: It's not clear if we need to distinguish (e.g.) list values of
|
||||
// fields from list values of typedefs. We could make {type,field} be
|
||||
// orthogonal to {scalar, list, list-value, map, map-key, map-value} (and
|
||||
// maybe even pointers?), but that seems like extra work that is not needed
|
||||
// for now.
|
||||
)
|
||||
|
||||
// Context describes where a tag was used, so that the scope can be checked
|
||||
// and so validators can handle different cases if they need.
|
||||
type Context struct {
|
||||
// Scope is where the validation is being considered.
|
||||
Scope Scope
|
||||
|
||||
// Type provides details about the type being validated. When Scope is
|
||||
// ScopeType, this is the underlying type. When Scope is ScopeField, this
|
||||
// is the field's type (including any pointerness). When Scope indicates a
|
||||
// list-value, map-key, or map-value, this is the type of that key or
|
||||
// value.
|
||||
Type *types.Type
|
||||
|
||||
// Parent provides details about the logical parent type of the type being
|
||||
// validated, when applicable. When Scope is ScopeType, this is the
|
||||
// newly-defined type (when it exists - gengo handles struct-type
|
||||
// definitions differently that other "alias" type definitions). When
|
||||
// Scope is ScopeField, this is the field's parent struct's type. When
|
||||
// Scope indicates a list-value, map-key, or map-value, this is the type of
|
||||
// the whole list or map.
|
||||
//
|
||||
// Because of how gengo handles struct-type definitions, this field may be
|
||||
// nil in those cases.
|
||||
Parent *types.Type
|
||||
|
||||
// Member provides details about a field within a struct, when Scope is
|
||||
// ScopeField. For all other values of Scope, this will be nil.
|
||||
Member *types.Member
|
||||
|
||||
// Path provides the field path to the type or field being validated. This
|
||||
// is useful for identifying an exact context, e.g. to track information
|
||||
// between related tags.
|
||||
Path *field.Path
|
||||
}
|
||||
|
||||
// TagDoc describes a comment-tag and its usage.
|
||||
type TagDoc struct {
|
||||
// Tag is the tag name, without the leading '+'.
|
||||
Tag string
|
||||
// Args lists any arguments this tag might take.
|
||||
Args []TagArgDoc
|
||||
// Usage is how the tag is used, including arguments.
|
||||
Usage string
|
||||
// Description is a short description of this tag's purpose.
|
||||
Description string
|
||||
// Docs is a human-oriented string explaining this tag.
|
||||
Docs string
|
||||
// Scopes lists the place or places this tag may be used.
|
||||
Scopes []Scope
|
||||
// Payloads lists zero or more varieties of value for this tag. If this tag
|
||||
// never has a payload, this list should be empty, but if the payload is
|
||||
// optional, this list should include an entry for "<none>".
|
||||
Payloads []TagPayloadDoc
|
||||
}
|
||||
|
||||
// TagArgDoc describes an argument for a tag (e.g. `+tagName(tagArg)`.
|
||||
type TagArgDoc struct {
|
||||
// Description is a short description of this arg (e.g. `<name>`).
|
||||
Description string
|
||||
}
|
||||
|
||||
// TagPayloadDoc describes a value for a tag (e.g. `+tagName=tagValue`). Some
|
||||
// tags upport multiple payloads, including <none> (e.g. `+tagName`).
|
||||
type TagPayloadDoc struct {
|
||||
// Description is a short description of this payload (e.g. `<number>`).
|
||||
Description string
|
||||
// Docs is a human-orientd string explaining this payload.
|
||||
Docs string
|
||||
// Schema details a JSON payload's contents.
|
||||
Schema []TagPayloadSchema
|
||||
}
|
||||
|
||||
// TagPayloadSchema describes a JSON tag payload.
|
||||
type TagPayloadSchema struct {
|
||||
Key string
|
||||
Value string
|
||||
Docs string
|
||||
Default string
|
||||
}
|
||||
|
||||
// Validations defines the function calls and variables to generate to perform validation.
|
||||
type Validations struct {
|
||||
Functions []FunctionGen
|
||||
Variables []VariableGen
|
||||
Comments []string
|
||||
OpaqueType bool
|
||||
OpaqueKeyType bool
|
||||
OpaqueValType bool
|
||||
}
|
||||
|
||||
func (v *Validations) Empty() bool {
|
||||
return v.Len() == 0
|
||||
}
|
||||
|
||||
func (v *Validations) Len() int {
|
||||
return len(v.Functions) + len(v.Variables) + len(v.Comments)
|
||||
}
|
||||
|
||||
func (v *Validations) AddFunction(f FunctionGen) {
|
||||
v.Functions = append(v.Functions, f)
|
||||
}
|
||||
|
||||
func (v *Validations) AddVariable(variable VariableGen) {
|
||||
v.Variables = append(v.Variables, variable)
|
||||
}
|
||||
|
||||
func (v *Validations) AddComment(comment string) {
|
||||
v.Comments = append(v.Comments, comment)
|
||||
}
|
||||
|
||||
func (v *Validations) Add(o Validations) {
|
||||
v.Functions = append(v.Functions, o.Functions...)
|
||||
v.Variables = append(v.Variables, o.Variables...)
|
||||
v.Comments = append(v.Comments, o.Comments...)
|
||||
v.OpaqueType = v.OpaqueType || o.OpaqueType
|
||||
v.OpaqueKeyType = v.OpaqueKeyType || o.OpaqueKeyType
|
||||
v.OpaqueValType = v.OpaqueValType || o.OpaqueValType
|
||||
}
|
||||
|
||||
// FunctionFlags define optional properties of a validator. Most validators
|
||||
// can just use DefaultFlags.
|
||||
type FunctionFlags uint32
|
||||
|
||||
// IsSet returns true if all of the wanted flags are set.
|
||||
func (ff FunctionFlags) IsSet(wanted FunctionFlags) bool {
|
||||
return (ff & wanted) == wanted
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultFlags is defined for clarity.
|
||||
DefaultFlags FunctionFlags = 0
|
||||
|
||||
// ShortCircuit indicates that further validations should be skipped if
|
||||
// this validator fails. Most validators are not fatal.
|
||||
ShortCircuit FunctionFlags = 1 << iota
|
||||
|
||||
// NonError indicates that a failure of this validator should not be
|
||||
// accumulated as an error, but should trigger other aspects of the failure
|
||||
// path (e.g. early return when combined with ShortCircuit).
|
||||
NonError
|
||||
)
|
||||
|
||||
// FunctionGen provides validation-gen with the information needed to generate a
|
||||
// validation function invocation.
|
||||
type FunctionGen interface {
|
||||
// TagName returns the tag which triggers this validator.
|
||||
TagName() string
|
||||
|
||||
// SignatureAndArgs returns the function name and all extraArg value literals that are passed when the function
|
||||
// invocation is generated.
|
||||
//
|
||||
// The function signature must be of the form:
|
||||
// func(op operation.Operation,
|
||||
// fldPath field.Path,
|
||||
// value, oldValue <ValueType>, // always nilable
|
||||
// extraArgs[0] <extraArgs[0]Type>, // optional
|
||||
// ...,
|
||||
// extraArgs[N] <extraArgs[N]Type>)
|
||||
//
|
||||
// extraArgs may contain:
|
||||
// - data literals comprised of maps, slices, strings, ints, floats and bools
|
||||
// - references, represented by types.Type (to reference any type in the universe), and types.Member (to reference members of the current value)
|
||||
//
|
||||
// If validation function to be called does not have a signature of this form, please introduce
|
||||
// a function that does and use that function to call the validation function.
|
||||
SignatureAndArgs() (function types.Name, extraArgs []any)
|
||||
|
||||
// TypeArgs assigns types to the type parameters of the function, for invocation.
|
||||
TypeArgs() []types.Name
|
||||
|
||||
// Flags returns the options for this validator function.
|
||||
Flags() FunctionFlags
|
||||
|
||||
// Conditions returns the conditions that must true for a resource to be
|
||||
// validated by this function.
|
||||
Conditions() Conditions
|
||||
}
|
||||
|
||||
// Conditions defines what conditions must be true for a resource to be validated.
|
||||
// If any of the conditions are not true, the resource is not validated.
|
||||
type Conditions struct {
|
||||
// OptionEnabled specifies an option name that must be set to true for the condition to be true.
|
||||
OptionEnabled string
|
||||
|
||||
// OptionDisabled specifies an option name that must be set to false for the condition to be true.
|
||||
OptionDisabled string
|
||||
}
|
||||
|
||||
func (c Conditions) Empty() bool {
|
||||
return len(c.OptionEnabled) == 0 && len(c.OptionDisabled) == 0
|
||||
}
|
||||
|
||||
// Identifier is a name that the generator will output as an identifier.
|
||||
// Identifiers are generated using the RawNamer strategy.
|
||||
type Identifier types.Name
|
||||
|
||||
// PrivateVar is a variable name that the generator will output as a private identifier.
|
||||
// PrivateVars are generated using the PrivateNamer strategy.
|
||||
type PrivateVar types.Name
|
||||
|
||||
// VariableGen provides validation-gen with the information needed to generate variable.
|
||||
// Variables typically support generated functions by providing static information such
|
||||
// as the list of supported symbols for an enum.
|
||||
type VariableGen interface {
|
||||
// TagName returns the tag which triggers this validator.
|
||||
TagName() string
|
||||
|
||||
// Var returns the variable identifier.
|
||||
Var() PrivateVar
|
||||
|
||||
// Init generates the function call that the variable is assigned to.
|
||||
Init() FunctionGen
|
||||
}
|
||||
|
||||
// Function creates a FunctionGen for a given function name and extraArgs.
|
||||
func Function(tagName string, flags FunctionFlags, function types.Name, extraArgs ...any) FunctionGen {
|
||||
return GenericFunction(tagName, flags, function, nil, extraArgs...)
|
||||
}
|
||||
|
||||
func GenericFunction(tagName string, flags FunctionFlags, function types.Name, typeArgs []types.Name, extraArgs ...any) FunctionGen {
|
||||
// Callers of Signature don't care if the args are all of a known type, it just
|
||||
// makes it easier to declare validators.
|
||||
var anyArgs []any
|
||||
if len(extraArgs) > 0 {
|
||||
anyArgs = make([]any, len(extraArgs))
|
||||
copy(anyArgs, extraArgs)
|
||||
}
|
||||
return &functionGen{tagName: tagName, flags: flags, function: function, extraArgs: anyArgs, typeArgs: typeArgs}
|
||||
}
|
||||
|
||||
func WithCondition(fn FunctionGen, conditions Conditions) FunctionGen {
|
||||
name, args := fn.SignatureAndArgs()
|
||||
return &functionGen{
|
||||
tagName: fn.TagName(), flags: fn.Flags(), function: name, extraArgs: args, typeArgs: fn.TypeArgs(),
|
||||
conditions: conditions,
|
||||
}
|
||||
}
|
||||
|
||||
type functionGen struct {
|
||||
tagName string
|
||||
function types.Name
|
||||
extraArgs []any
|
||||
typeArgs []types.Name
|
||||
flags FunctionFlags
|
||||
conditions Conditions
|
||||
}
|
||||
|
||||
func (v *functionGen) TagName() string {
|
||||
return v.tagName
|
||||
}
|
||||
|
||||
func (v *functionGen) SignatureAndArgs() (function types.Name, args []any) {
|
||||
return v.function, v.extraArgs
|
||||
}
|
||||
|
||||
func (v *functionGen) TypeArgs() []types.Name { return v.typeArgs }
|
||||
|
||||
func (v *functionGen) Flags() FunctionFlags {
|
||||
return v.flags
|
||||
}
|
||||
|
||||
func (v *functionGen) Conditions() Conditions { return v.conditions }
|
||||
|
||||
// Variable creates a VariableGen for a given function name and extraArgs.
|
||||
func Variable(variable PrivateVar, init FunctionGen) VariableGen {
|
||||
return &variableGen{
|
||||
variable: variable,
|
||||
init: init,
|
||||
}
|
||||
}
|
||||
|
||||
type variableGen struct {
|
||||
variable PrivateVar
|
||||
init FunctionGen
|
||||
}
|
||||
|
||||
func (v variableGen) TagName() string {
|
||||
return v.init.TagName()
|
||||
}
|
||||
|
||||
func (v variableGen) Var() PrivateVar {
|
||||
return v.variable
|
||||
}
|
||||
|
||||
func (v variableGen) Init() FunctionGen {
|
||||
return v.init
|
||||
}
|
||||
|
||||
// WrapperFunction describes a function literal which has the fingerprint of a
|
||||
// regular validation function (op, fldPath, obj, oldObj) and calls another
|
||||
// validation function with the same signature, plus extra args if needed.
|
||||
type WrapperFunction struct {
|
||||
Function FunctionGen
|
||||
ObjType *types.Type
|
||||
}
|
||||
|
||||
// Literal is a literal value that, when used as an argument to a validator,
|
||||
// will be emitted without any further interpretation. Use this with caution,
|
||||
// it will not be subject to Namers.
|
||||
type Literal string
|
||||
|
||||
// FunctionLiteral describes a function-literal expression that can be used as
|
||||
// an argument to a validator. Unlike WrapperFunction, this does not
|
||||
// necessarily have the same signature as a regular validation function.
|
||||
type FunctionLiteral struct {
|
||||
Parameters []ParamResult
|
||||
Results []ParamResult
|
||||
Body string
|
||||
}
|
||||
|
||||
// ParamResult represents a parameter or a result of a function.
|
||||
type ParamResult struct {
|
||||
Name string
|
||||
Type *types.Type
|
||||
}
|
Loading…
Reference in New Issue
Block a user