mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 18:28:13 +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:
		| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Joe Betz
					Joe Betz