add prune and remove unused packages

This commit is contained in:
Michelle Au
2019-03-08 14:54:43 -08:00
parent f59b58d164
commit 8c0accad66
17240 changed files with 27 additions and 4750030 deletions

View File

@@ -1,49 +0,0 @@
# Generate OpenAPI definitions
- To generate definition for a specific type or package add "+k8s:openapi-gen=true" tag to the type/package comment lines.
- To exclude a type or a member from a tagged package/type, add "+k8s:openapi-gen=false" tag to the comment lines.
# OpenAPI Extensions
OpenAPI spec can have extensions on types. To define one or more extensions on a type or its member
add `+k8s:openapi-gen=x-kubernetes-$NAME:$VALUE` to the comment lines before type/member. A type/member can
have multiple extensions. The rest of the line in the comment will be used as $VALUE so there is no need to
escape or quote the value string. Extensions can be used to pass more information to client generators or
documentation generators. For example a type might have a friendly name to be displayed in documentation or
being used in a client's fluent interface.
# Custom OpenAPI type definitions
Custom types which otherwise don't map directly to OpenAPI can override their
OpenAPI definition by implementing a function named "OpenAPIDefinition" with
the following signature:
```go
import openapi "k8s.io/kube-openapi/pkg/common"
// ...
type Time struct {
time.Time
}
func (_ Time) OpenAPIDefinition() openapi.OpenAPIDefinition {
return openapi.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
},
},
}
}
```
Alternatively, the type can avoid the "openapi" import by defining the following
methods. The following example produces the same OpenAPI definition as the
example above:
```go
func (_ Time) OpenAPISchemaType() []string { return []string{"string"} }
func (_ Time) OpenAPISchemaFormat() string { return "date-time" }
```

View File

@@ -1,219 +0,0 @@
/*
Copyright 2018 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 generators
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"k8s.io/kube-openapi/pkg/generators/rules"
"k8s.io/gengo/generator"
"k8s.io/gengo/types"
"k8s.io/klog"
)
const apiViolationFileType = "api-violation"
type apiViolationFile struct {
// Since our file actually is unrelated to the package structure, use a
// path that hasn't been mangled by the framework.
unmangledPath string
}
func (a apiViolationFile) AssembleFile(f *generator.File, path string) error {
path = a.unmangledPath
klog.V(2).Infof("Assembling file %q", path)
if path == "-" {
_, err := io.Copy(os.Stdout, &f.Body)
return err
}
output, err := os.Create(path)
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(output, &f.Body)
return err
}
func (a apiViolationFile) VerifyFile(f *generator.File, path string) error {
if path == "-" {
// Nothing to verify against.
return nil
}
path = a.unmangledPath
formatted := f.Body.Bytes()
existing, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("unable to read file %q for comparison: %v", path, err)
}
if bytes.Compare(formatted, existing) == 0 {
return nil
}
// Be nice and find the first place where they differ
// (Copied from gengo's default file type)
i := 0
for i < len(formatted) && i < len(existing) && formatted[i] == existing[i] {
i++
}
eDiff, fDiff := existing[i:], formatted[i:]
if len(eDiff) > 100 {
eDiff = eDiff[:100]
}
if len(fDiff) > 100 {
fDiff = fDiff[:100]
}
return fmt.Errorf("output for %q differs; first existing/expected diff: \n %q\n %q", path, string(eDiff), string(fDiff))
}
func newAPIViolationGen() *apiViolationGen {
return &apiViolationGen{
linter: newAPILinter(),
}
}
type apiViolationGen struct {
generator.DefaultGen
linter *apiLinter
}
func (v *apiViolationGen) FileType() string { return apiViolationFileType }
func (v *apiViolationGen) Filename() string {
return "this file is ignored by the file assembler"
}
func (v *apiViolationGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
klog.V(5).Infof("validating API rules for type %v", t)
if err := v.linter.validate(t); err != nil {
return err
}
return nil
}
// Finalize prints the API rule violations to report file (if specified from
// arguments) or stdout (default)
func (v *apiViolationGen) Finalize(c *generator.Context, w io.Writer) error {
// NOTE: we don't return error here because we assume that the report file will
// get evaluated afterwards to determine if error should be raised. For example,
// you can have make rules that compare the report file with existing known
// violations (whitelist) and determine no error if no change is detected.
v.linter.report(w)
return nil
}
// apiLinter is the framework hosting multiple API rules and recording API rule
// violations
type apiLinter struct {
// API rules that implement APIRule interface and output API rule violations
rules []APIRule
violations []apiViolation
}
// newAPILinter creates an apiLinter object with API rules in package rules. Please
// add APIRule here when new API rule is implemented.
func newAPILinter() *apiLinter {
return &apiLinter{
rules: []APIRule{
&rules.NamesMatch{},
&rules.OmitEmptyMatchCase{},
},
}
}
// apiViolation uniquely identifies single API rule violation
type apiViolation struct {
// Name of rule from APIRule.Name()
rule string
packageName string
typeName string
// Optional: name of field that violates API rule. Empty fieldName implies that
// the entire type violates the rule.
field string
}
// apiViolations implements sort.Interface for []apiViolation based on the fields: rule,
// packageName, typeName and field.
type apiViolations []apiViolation
func (a apiViolations) Len() int { return len(a) }
func (a apiViolations) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a apiViolations) Less(i, j int) bool {
if a[i].rule != a[j].rule {
return a[i].rule < a[j].rule
}
if a[i].packageName != a[j].packageName {
return a[i].packageName < a[j].packageName
}
if a[i].typeName != a[j].typeName {
return a[i].typeName < a[j].typeName
}
return a[i].field < a[j].field
}
// APIRule is the interface for validating API rule on Go types
type APIRule interface {
// Validate evaluates API rule on type t and returns a list of field names in
// the type that violate the rule. Empty field name [""] implies the entire
// type violates the rule.
Validate(t *types.Type) ([]string, error)
// Name returns the name of APIRule
Name() string
}
// validate runs all API rules on type t and records any API rule violation
func (l *apiLinter) validate(t *types.Type) error {
for _, r := range l.rules {
klog.V(5).Infof("validating API rule %v for type %v", r.Name(), t)
fields, err := r.Validate(t)
if err != nil {
return err
}
for _, field := range fields {
l.violations = append(l.violations, apiViolation{
rule: r.Name(),
packageName: t.Name.Package,
typeName: t.Name.Name,
field: field,
})
}
}
return nil
}
// report prints any API rule violation to writer w and returns error if violation exists
func (l *apiLinter) report(w io.Writer) error {
sort.Sort(apiViolations(l.violations))
for _, v := range l.violations {
fmt.Fprintf(w, "API rule violation: %s,%s,%s,%s\n", v.rule, v.packageName, v.typeName, v.field)
}
if len(l.violations) > 0 {
return fmt.Errorf("API rule violations exist")
}
return nil
}

View File

@@ -1,91 +0,0 @@
/*
Copyright 2018 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 generators
import (
"fmt"
"path/filepath"
"k8s.io/gengo/args"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
"k8s.io/klog"
generatorargs "k8s.io/kube-openapi/cmd/openapi-gen/args"
)
type identityNamer struct{}
func (_ identityNamer) Name(t *types.Type) string {
return t.Name.String()
}
var _ namer.Namer = identityNamer{}
// NameSystems returns the name system used by the generators in this package.
func NameSystems() namer.NameSystems {
return namer.NameSystems{
"raw": namer.NewRawNamer("", nil),
"sorting_namer": identityNamer{},
}
}
// DefaultNameSystem returns the default name system for ordering the types to be
// processed by the generators in this package.
func DefaultNameSystem() string {
return "sorting_namer"
}
func Packages(context *generator.Context, arguments *args.GeneratorArgs) generator.Packages {
boilerplate, err := arguments.LoadGoBoilerplate()
if err != nil {
klog.Fatalf("Failed loading boilerplate: %v", err)
}
header := append([]byte(fmt.Sprintf("// +build !%s\n\n", arguments.GeneratedBuildTag)), boilerplate...)
header = append(header, []byte(
`
// This file was autogenerated by openapi-gen. Do not edit it manually!
`)...)
reportPath := "-"
if customArgs, ok := arguments.CustomArgs.(*generatorargs.CustomArgs); ok {
reportPath = customArgs.ReportFilename
}
context.FileTypes[apiViolationFileType] = apiViolationFile{
unmangledPath: reportPath,
}
return generator.Packages{
&generator.DefaultPackage{
PackageName: filepath.Base(arguments.OutputPackagePath),
PackagePath: arguments.OutputPackagePath,
HeaderText: header,
GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
return []generator.Generator{
newOpenAPIGen(
arguments.OutputFileBaseName,
arguments.OutputPackagePath,
),
newAPIViolationGen(),
}
},
FilterFunc: apiTypeFilterFunc,
},
}
}

View File

@@ -1,182 +0,0 @@
/*
Copyright 2018 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 generators
import (
"fmt"
"sort"
"strings"
"k8s.io/gengo/examples/set-gen/sets"
"k8s.io/gengo/types"
)
const extensionPrefix = "x-kubernetes-"
// extensionAttributes encapsulates common traits for particular extensions.
type extensionAttributes struct {
xName string
kind types.Kind
allowedValues sets.String
}
// Extension tag to openapi extension attributes
var tagToExtension = map[string]extensionAttributes{
"patchMergeKey": {
xName: "x-kubernetes-patch-merge-key",
kind: types.Slice,
},
"patchStrategy": {
xName: "x-kubernetes-patch-strategy",
kind: types.Slice,
allowedValues: sets.NewString("merge", "retainKeys"),
},
"listMapKey": {
xName: "x-kubernetes-list-map-keys",
kind: types.Slice,
},
"listType": {
xName: "x-kubernetes-list-type",
kind: types.Slice,
allowedValues: sets.NewString("atomic", "set", "map"),
},
}
// Extension encapsulates information necessary to generate an OpenAPI extension.
type extension struct {
idlTag string // Example: listType
xName string // Example: x-kubernetes-list-type
values []string // Example: [atomic]
}
func (e extension) hasAllowedValues() bool {
return tagToExtension[e.idlTag].allowedValues.Len() > 0
}
func (e extension) allowedValues() sets.String {
return tagToExtension[e.idlTag].allowedValues
}
func (e extension) hasKind() bool {
return len(tagToExtension[e.idlTag].kind) > 0
}
func (e extension) kind() types.Kind {
return tagToExtension[e.idlTag].kind
}
func (e extension) validateAllowedValues() error {
// allowedValues not set means no restrictions on values.
if !e.hasAllowedValues() {
return nil
}
// Check for missing value.
if len(e.values) == 0 {
return fmt.Errorf("%s needs a value, none given.", e.idlTag)
}
// For each extension value, validate that it is allowed.
allowedValues := e.allowedValues()
if !allowedValues.HasAll(e.values...) {
return fmt.Errorf("%v not allowed for %s. Allowed values: %v",
e.values, e.idlTag, allowedValues.List())
}
return nil
}
func (e extension) validateType(kind types.Kind) error {
// If this extension class has no kind, then don't validate the type.
if !e.hasKind() {
return nil
}
if kind != e.kind() {
return fmt.Errorf("tag %s on type %v; only allowed on type %v",
e.idlTag, kind, e.kind())
}
return nil
}
func (e extension) hasMultipleValues() bool {
return len(e.values) > 1
}
// Returns sorted list of map keys. Needed for deterministic testing.
func sortedMapKeys(m map[string][]string) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
sort.Strings(keys)
return keys
}
// Parses comments to return openapi extensions. Returns a list of
// extensions which parsed correctly, as well as a list of the
// parse errors. Validating extensions is performed separately.
// NOTE: Non-empty errors does not mean extensions is empty.
func parseExtensions(comments []string) ([]extension, []error) {
extensions := []extension{}
errors := []error{}
// First, generate extensions from "+k8s:openapi-gen=x-kubernetes-*" annotations.
values := getOpenAPITagValue(comments)
for _, val := range values {
// Example: x-kubernetes-member-tag:member_test
if strings.HasPrefix(val, extensionPrefix) {
parts := strings.SplitN(val, ":", 2)
if len(parts) != 2 {
errors = append(errors, fmt.Errorf("invalid extension value: %v", val))
continue
}
e := extension{
idlTag: tagName, // Example: k8s:openapi-gen
xName: parts[0], // Example: x-kubernetes-member-tag
values: []string{parts[1]}, // Example: member_test
}
extensions = append(extensions, e)
}
}
// Next, generate extensions from "idlTags" (e.g. +listType)
tagValues := types.ExtractCommentTags("+", comments)
for _, idlTag := range sortedMapKeys(tagValues) {
xAttrs, exists := tagToExtension[idlTag]
if !exists {
continue
}
values := tagValues[idlTag]
e := extension{
idlTag: idlTag, // listType
xName: xAttrs.xName, // x-kubernetes-list-type
values: values, // [atomic]
}
extensions = append(extensions, e)
}
return extensions, errors
}
func validateMemberExtensions(extensions []extension, m *types.Member) []error {
errors := []error{}
for _, e := range extensions {
if err := e.validateAllowedValues(); err != nil {
errors = append(errors, err)
}
if err := e.validateType(m.Type.Kind); err != nil {
errors = append(errors, err)
}
}
return errors
}

View File

@@ -1,454 +0,0 @@
/*
Copyright 2018 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 generators
import (
"reflect"
"strings"
"testing"
"k8s.io/gengo/examples/set-gen/sets"
"k8s.io/gengo/types"
)
func TestSingleTagExtension(t *testing.T) {
// Comments only contain one tag extension and one value.
var tests = []struct {
comments []string
extensionTag string
extensionName string
extensionValues []string
}{
{
comments: []string{"+patchMergeKey=name"},
extensionTag: "patchMergeKey",
extensionName: "x-kubernetes-patch-merge-key",
extensionValues: []string{"name"},
},
{
comments: []string{"+patchStrategy=merge"},
extensionTag: "patchStrategy",
extensionName: "x-kubernetes-patch-strategy",
extensionValues: []string{"merge"},
},
{
comments: []string{"+listType=atomic"},
extensionTag: "listType",
extensionName: "x-kubernetes-list-type",
extensionValues: []string{"atomic"},
},
{
comments: []string{"+listMapKey=port"},
extensionTag: "listMapKey",
extensionName: "x-kubernetes-list-map-keys",
extensionValues: []string{"port"},
},
{
comments: []string{"+k8s:openapi-gen=x-kubernetes-member-tag:member_test"},
extensionTag: "k8s:openapi-gen",
extensionName: "x-kubernetes-member-tag",
extensionValues: []string{"member_test"},
},
{
comments: []string{"+k8s:openapi-gen=x-kubernetes-member-tag:member_test:member_test2"},
extensionTag: "k8s:openapi-gen",
extensionName: "x-kubernetes-member-tag",
extensionValues: []string{"member_test:member_test2"},
},
{
// Test that poorly formatted extensions aren't added.
comments: []string{
"+k8s:openapi-gen=x-kubernetes-no-value",
"+k8s:openapi-gen=x-kubernetes-member-success:success",
"+k8s:openapi-gen=x-kubernetes-wrong-separator;error",
},
extensionTag: "k8s:openapi-gen",
extensionName: "x-kubernetes-member-success",
extensionValues: []string{"success"},
},
}
for _, test := range tests {
extensions, _ := parseExtensions(test.comments)
actual := extensions[0]
if actual.idlTag != test.extensionTag {
t.Errorf("Extension Tag: expected (%s), actual (%s)\n", test.extensionTag, actual.idlTag)
}
if actual.xName != test.extensionName {
t.Errorf("Extension Name: expected (%s), actual (%s)\n", test.extensionName, actual.xName)
}
if !reflect.DeepEqual(actual.values, test.extensionValues) {
t.Errorf("Extension Values: expected (%s), actual (%s)\n", test.extensionValues, actual.values)
}
if actual.hasMultipleValues() {
t.Errorf("%s: hasMultipleValues() should be false\n", actual.xName)
}
}
}
func TestMultipleTagExtensions(t *testing.T) {
var tests = []struct {
comments []string
extensionTag string
extensionName string
extensionValues []string
}{
{
comments: []string{
"+listMapKey=port",
"+listMapKey=protocol",
},
extensionTag: "listMapKey",
extensionName: "x-kubernetes-list-map-keys",
extensionValues: []string{"port", "protocol"},
},
}
for _, test := range tests {
extensions, errors := parseExtensions(test.comments)
if len(errors) > 0 {
t.Errorf("Unexpected errors: %v\n", errors)
}
actual := extensions[0]
if actual.idlTag != test.extensionTag {
t.Errorf("Extension Tag: expected (%s), actual (%s)\n", test.extensionTag, actual.idlTag)
}
if actual.xName != test.extensionName {
t.Errorf("Extension Name: expected (%s), actual (%s)\n", test.extensionName, actual.xName)
}
if !reflect.DeepEqual(actual.values, test.extensionValues) {
t.Errorf("Extension Values: expected (%s), actual (%s)\n", test.extensionValues, actual.values)
}
if !actual.hasMultipleValues() {
t.Errorf("%s: hasMultipleValues() should be true\n", actual.xName)
}
}
}
func TestExtensionParseErrors(t *testing.T) {
var tests = []struct {
comments []string
errorMessage string
}{
{
// Missing extension value should be an error.
comments: []string{
"+k8s:openapi-gen=x-kubernetes-no-value",
},
errorMessage: "x-kubernetes-no-value",
},
{
// Wrong separator should be an error.
comments: []string{
"+k8s:openapi-gen=x-kubernetes-wrong-separator;error",
},
errorMessage: "x-kubernetes-wrong-separator;error",
},
}
for _, test := range tests {
_, errors := parseExtensions(test.comments)
if len(errors) == 0 {
t.Errorf("Expected errors while parsing: %v\n", test.comments)
}
error := errors[0]
if !strings.Contains(error.Error(), test.errorMessage) {
t.Errorf("Error (%v) should contain substring (%s)\n", error, test.errorMessage)
}
}
}
func TestExtensionAllowedValues(t *testing.T) {
var methodTests = []struct {
e extension
allowedValues sets.String
}{
{
e: extension{
idlTag: "patchStrategy",
},
allowedValues: sets.NewString("merge", "retainKeys"),
},
{
e: extension{
idlTag: "patchMergeKey",
},
allowedValues: nil,
},
{
e: extension{
idlTag: "listType",
},
allowedValues: sets.NewString("atomic", "set", "map"),
},
{
e: extension{
idlTag: "listMapKey",
},
allowedValues: nil,
},
{
e: extension{
idlTag: "k8s:openapi-gen",
},
allowedValues: nil,
},
}
for _, test := range methodTests {
if test.allowedValues != nil {
if !test.e.hasAllowedValues() {
t.Errorf("hasAllowedValues() expected (true), but received: false")
}
if !reflect.DeepEqual(test.allowedValues, test.e.allowedValues()) {
t.Errorf("allowedValues() expected (%v), but received: %v",
test.allowedValues, test.e.allowedValues())
}
}
if test.allowedValues == nil && test.e.hasAllowedValues() {
t.Errorf("hasAllowedValues() expected (false), but received: true")
}
}
var successTests = []struct {
e extension
}{
{
e: extension{
idlTag: "patchStrategy",
xName: "x-kubernetes-patch-strategy",
values: []string{"merge"},
},
},
{
// Validate multiple values.
e: extension{
idlTag: "patchStrategy",
xName: "x-kubernetes-patch-strategy",
values: []string{"merge", "retainKeys"},
},
},
{
e: extension{
idlTag: "patchMergeKey",
xName: "x-kubernetes-patch-merge-key",
values: []string{"key1"},
},
},
{
e: extension{
idlTag: "listType",
xName: "x-kubernetes-list-type",
values: []string{"atomic"},
},
},
}
for _, test := range successTests {
actualErr := test.e.validateAllowedValues()
if actualErr != nil {
t.Errorf("Expected no error for (%v), but received: %v\n", test.e, actualErr)
}
}
var failureTests = []struct {
e extension
}{
{
// Every value must be allowed.
e: extension{
idlTag: "patchStrategy",
xName: "x-kubernetes-patch-strategy",
values: []string{"disallowed", "merge"},
},
},
{
e: extension{
idlTag: "patchStrategy",
xName: "x-kubernetes-patch-strategy",
values: []string{"foo"},
},
},
{
e: extension{
idlTag: "listType",
xName: "x-kubernetes-list-type",
values: []string{"not-allowed"},
},
},
}
for _, test := range failureTests {
actualErr := test.e.validateAllowedValues()
if actualErr == nil {
t.Errorf("Expected error, but received none: %v\n", test.e)
}
}
}
func TestExtensionKind(t *testing.T) {
var methodTests = []struct {
e extension
kind types.Kind
}{
{
e: extension{
idlTag: "patchStrategy",
},
kind: types.Slice,
},
{
e: extension{
idlTag: "patchMergeKey",
},
kind: types.Slice,
},
{
e: extension{
idlTag: "listType",
},
kind: types.Slice,
},
{
e: extension{
idlTag: "listMapKey",
},
kind: types.Slice,
},
{
e: extension{
idlTag: "k8s:openapi-gen",
},
kind: "",
},
}
for _, test := range methodTests {
if len(test.kind) > 0 {
if !test.e.hasKind() {
t.Errorf("%v: hasKind() expected (true), but received: false", test.e)
}
if test.kind != test.e.kind() {
t.Errorf("%v: kind() expected (%v), but received: %v", test.e, test.kind, test.e.kind())
}
} else {
if test.e.hasKind() {
t.Errorf("%v: hasKind() expected (false), but received: true", test.e)
}
}
}
}
func TestValidateMemberExtensions(t *testing.T) {
patchStrategyExtension := extension{
idlTag: "patchStrategy",
xName: "x-kubernetes-patch-strategy",
values: []string{"merge"},
}
patchMergeKeyExtension := extension{
idlTag: "patchMergeKey",
xName: "x-kubernetes-patch-merge-key",
values: []string{"key1", "key2"},
}
listTypeExtension := extension{
idlTag: "listType",
xName: "x-kubernetes-list-type",
values: []string{"atomic"},
}
listMapKeysExtension := extension{
idlTag: "listMapKey",
xName: "x-kubernetes-map-keys",
values: []string{"key1"},
}
genExtension := extension{
idlTag: "k8s:openapi-gen",
xName: "x-kubernetes-member-type",
values: []string{"value1"},
}
sliceField := types.Member{
Name: "Containers",
Type: &types.Type{
Kind: types.Slice,
},
}
mapField := types.Member{
Name: "Containers",
Type: &types.Type{
Kind: types.Map,
},
}
var successTests = []struct {
extensions []extension
member types.Member
}{
// Test single member extension
{
extensions: []extension{patchStrategyExtension},
member: sliceField,
},
// Test multiple member extensions
{
extensions: []extension{
patchMergeKeyExtension,
listTypeExtension,
listMapKeysExtension,
genExtension, // Should not generate errors during type validation
},
member: sliceField,
},
}
for _, test := range successTests {
errors := validateMemberExtensions(test.extensions, &test.member)
if len(errors) > 0 {
t.Errorf("validateMemberExtensions: %v should have produced no errors. Errors: %v",
test.extensions, errors)
}
}
var failureTests = []struct {
extensions []extension
member types.Member
}{
// Test single member extension
{
extensions: []extension{patchStrategyExtension},
member: mapField,
},
// Test multiple member extensions
{
extensions: []extension{
patchMergeKeyExtension,
listTypeExtension,
listMapKeysExtension,
},
member: mapField,
},
}
for _, test := range failureTests {
errors := validateMemberExtensions(test.extensions, &test.member)
if len(errors) != len(test.extensions) {
t.Errorf("validateMemberExtensions: %v should have produced all errors. Errors: %v",
test.extensions, errors)
}
}
}

View File

@@ -1,607 +0,0 @@
/*
Copyright 2016 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 generators
import (
"bytes"
"fmt"
"io"
"path/filepath"
"reflect"
"sort"
"strings"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/types"
openapi "k8s.io/kube-openapi/pkg/common"
"k8s.io/klog"
)
// This is the comment tag that carries parameters for open API generation.
const tagName = "k8s:openapi-gen"
const tagOptional = "optional"
// Known values for the tag.
const (
tagValueTrue = "true"
tagValueFalse = "false"
)
// Used for temporary validation of patch struct tags.
// TODO: Remove patch struct tag validation because they we are now consuming OpenAPI on server.
var tempPatchTags = [...]string{
"patchMergeKey",
"patchStrategy",
}
func getOpenAPITagValue(comments []string) []string {
return types.ExtractCommentTags("+", comments)[tagName]
}
func getSingleTagsValue(comments []string, tag string) (string, error) {
tags, ok := types.ExtractCommentTags("+", comments)[tag]
if !ok || len(tags) == 0 {
return "", nil
}
if len(tags) > 1 {
return "", fmt.Errorf("multiple values are not allowed for tag %s", tag)
}
return tags[0], nil
}
func hasOpenAPITagValue(comments []string, value string) bool {
tagValues := getOpenAPITagValue(comments)
for _, val := range tagValues {
if val == value {
return true
}
}
return false
}
// hasOptionalTag returns true if the member has +optional in its comments or
// omitempty in its json tags.
func hasOptionalTag(m *types.Member) bool {
hasOptionalCommentTag := types.ExtractCommentTags(
"+", m.CommentLines)[tagOptional] != nil
hasOptionalJsonTag := strings.Contains(
reflect.StructTag(m.Tags).Get("json"), "omitempty")
return hasOptionalCommentTag || hasOptionalJsonTag
}
func apiTypeFilterFunc(c *generator.Context, t *types.Type) bool {
// There is a conflict between this codegen and codecgen, we should avoid types generated for codecgen
if strings.HasPrefix(t.Name.Name, "codecSelfer") {
return false
}
pkg := c.Universe.Package(t.Name.Package)
if hasOpenAPITagValue(pkg.Comments, tagValueTrue) {
return !hasOpenAPITagValue(t.CommentLines, tagValueFalse)
}
if hasOpenAPITagValue(t.CommentLines, tagValueTrue) {
return true
}
return false
}
const (
specPackagePath = "github.com/go-openapi/spec"
openAPICommonPackagePath = "k8s.io/kube-openapi/pkg/common"
)
// openApiGen produces a file with auto-generated OpenAPI functions.
type openAPIGen struct {
generator.DefaultGen
// TargetPackage is the package that will get GetOpenAPIDefinitions function returns all open API definitions.
targetPackage string
imports namer.ImportTracker
}
func newOpenAPIGen(sanitizedName string, targetPackage string) generator.Generator {
return &openAPIGen{
DefaultGen: generator.DefaultGen{
OptionalName: sanitizedName,
},
imports: generator.NewImportTracker(),
targetPackage: targetPackage,
}
}
const nameTmpl = "schema_$.type|private$"
func (g *openAPIGen) Namers(c *generator.Context) namer.NameSystems {
// Have the raw namer for this file track what it imports.
return namer.NameSystems{
"raw": namer.NewRawNamer(g.targetPackage, g.imports),
"private": &namer.NameStrategy{
Join: func(pre string, in []string, post string) string {
return strings.Join(in, "_")
},
PrependPackageNames: 4, // enough to fully qualify from k8s.io/api/...
},
}
}
func (g *openAPIGen) isOtherPackage(pkg string) bool {
if pkg == g.targetPackage {
return false
}
if strings.HasSuffix(pkg, "\""+g.targetPackage+"\"") {
return false
}
return true
}
func (g *openAPIGen) Imports(c *generator.Context) []string {
importLines := []string{}
for _, singleImport := range g.imports.ImportLines() {
importLines = append(importLines, singleImport)
}
return importLines
}
func argsFromType(t *types.Type) generator.Args {
return generator.Args{
"type": t,
"ReferenceCallback": types.Ref(openAPICommonPackagePath, "ReferenceCallback"),
"OpenAPIDefinition": types.Ref(openAPICommonPackagePath, "OpenAPIDefinition"),
"SpecSchemaType": types.Ref(specPackagePath, "Schema"),
}
}
func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error {
sw := generator.NewSnippetWriter(w, c, "$", "$")
sw.Do("func GetOpenAPIDefinitions(ref $.ReferenceCallback|raw$) map[string]$.OpenAPIDefinition|raw$ {\n", argsFromType(nil))
sw.Do("return map[string]$.OpenAPIDefinition|raw${\n", argsFromType(nil))
for _, t := range c.Order {
err := newOpenAPITypeWriter(sw).generateCall(t)
if err != nil {
return err
}
}
sw.Do("}\n", nil)
sw.Do("}\n\n", nil)
return sw.Error()
}
func (g *openAPIGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
klog.V(5).Infof("generating for type %v", t)
sw := generator.NewSnippetWriter(w, c, "$", "$")
err := newOpenAPITypeWriter(sw).generate(t)
if err != nil {
return err
}
return sw.Error()
}
func getJsonTags(m *types.Member) []string {
jsonTag := reflect.StructTag(m.Tags).Get("json")
if jsonTag == "" {
return []string{}
}
return strings.Split(jsonTag, ",")
}
func getReferableName(m *types.Member) string {
jsonTags := getJsonTags(m)
if len(jsonTags) > 0 {
if jsonTags[0] == "-" {
return ""
} else {
return jsonTags[0]
}
} else {
return m.Name
}
}
func shouldInlineMembers(m *types.Member) bool {
jsonTags := getJsonTags(m)
return len(jsonTags) > 1 && jsonTags[1] == "inline"
}
type openAPITypeWriter struct {
*generator.SnippetWriter
refTypes map[string]*types.Type
GetDefinitionInterface *types.Type
}
func newOpenAPITypeWriter(sw *generator.SnippetWriter) openAPITypeWriter {
return openAPITypeWriter{
SnippetWriter: sw,
refTypes: map[string]*types.Type{},
}
}
func methodReturnsValue(mt *types.Type, pkg, name string) bool {
if len(mt.Signature.Parameters) != 0 || len(mt.Signature.Results) != 1 {
return false
}
r := mt.Signature.Results[0]
return r.Name.Name == name && r.Name.Package == pkg
}
func hasOpenAPIDefinitionMethod(t *types.Type) bool {
for mn, mt := range t.Methods {
if mn != "OpenAPIDefinition" {
continue
}
return methodReturnsValue(mt, openAPICommonPackagePath, "OpenAPIDefinition")
}
return false
}
func hasOpenAPIDefinitionMethods(t *types.Type) bool {
var hasSchemaTypeMethod, hasOpenAPISchemaFormat bool
for mn, mt := range t.Methods {
switch mn {
case "OpenAPISchemaType":
hasSchemaTypeMethod = methodReturnsValue(mt, "", "[]string")
case "OpenAPISchemaFormat":
hasOpenAPISchemaFormat = methodReturnsValue(mt, "", "string")
}
}
return hasSchemaTypeMethod && hasOpenAPISchemaFormat
}
// typeShortName returns short package name (e.g. the name x appears in package x definition) dot type name.
func typeShortName(t *types.Type) string {
return filepath.Base(t.Name.Package) + "." + t.Name.Name
}
func (g openAPITypeWriter) generateMembers(t *types.Type, required []string) ([]string, error) {
var err error
for _, m := range t.Members {
if hasOpenAPITagValue(m.CommentLines, tagValueFalse) {
continue
}
if shouldInlineMembers(&m) {
required, err = g.generateMembers(m.Type, required)
if err != nil {
return required, err
}
continue
}
name := getReferableName(&m)
if name == "" {
continue
}
if !hasOptionalTag(&m) {
required = append(required, name)
}
if err = g.generateProperty(&m, t); err != nil {
klog.Errorf("Error when generating: %v, %v\n", name, m)
return required, err
}
}
return required, nil
}
func (g openAPITypeWriter) generateCall(t *types.Type) error {
// Only generate for struct type and ignore the rest
switch t.Kind {
case types.Struct:
args := argsFromType(t)
g.Do("\"$.$\": ", t.Name)
if hasOpenAPIDefinitionMethod(t) {
g.Do("$.type|raw${}.OpenAPIDefinition(),\n", args)
} else {
g.Do(nameTmpl+"(ref),\n", args)
}
}
return g.Error()
}
func (g openAPITypeWriter) generate(t *types.Type) error {
// Only generate for struct type and ignore the rest
switch t.Kind {
case types.Struct:
if hasOpenAPIDefinitionMethod(t) {
// already invoked directly
return nil
}
args := argsFromType(t)
g.Do("func "+nameTmpl+"(ref $.ReferenceCallback|raw$) $.OpenAPIDefinition|raw$ {\n", args)
if hasOpenAPIDefinitionMethods(t) {
g.Do("return $.OpenAPIDefinition|raw${\n"+
"Schema: spec.Schema{\n"+
"SchemaProps: spec.SchemaProps{\n", args)
g.generateDescription(t.CommentLines)
g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+
"Format:$.type|raw${}.OpenAPISchemaFormat(),\n"+
"},\n"+
"},\n"+
"}\n}\n\n", args)
return nil
}
g.Do("return $.OpenAPIDefinition|raw${\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", args)
g.generateDescription(t.CommentLines)
g.Do("Properties: map[string]$.SpecSchemaType|raw${\n", args)
required, err := g.generateMembers(t, []string{})
if err != nil {
return err
}
g.Do("},\n", nil)
if len(required) > 0 {
g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\""))
}
g.Do("},\n", nil)
if err := g.generateStructExtensions(t); err != nil {
return err
}
g.Do("},\n", nil)
g.Do("Dependencies: []string{\n", args)
// Map order is undefined, sort them or we may get a different file generated each time.
keys := []string{}
for k := range g.refTypes {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := g.refTypes[k]
if t, _ := openapi.GetOpenAPITypeFormat(v.String()); t != "" {
// This is a known type, we do not need a reference to it
// Will eliminate special case of time.Time
continue
}
g.Do("\"$.$\",", k)
}
g.Do("},\n}\n}\n\n", nil)
}
return nil
}
func (g openAPITypeWriter) generateStructExtensions(t *types.Type) error {
extensions, errors := parseExtensions(t.CommentLines)
// Initially, we will only log struct extension errors.
if len(errors) > 0 {
for _, e := range errors {
klog.V(2).Infof("[%s]: %s\n", t.String(), e)
}
}
// TODO(seans3): Validate struct extensions here.
g.emitExtensions(extensions)
return nil
}
func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type) error {
extensions, parseErrors := parseExtensions(m.CommentLines)
validationErrors := validateMemberExtensions(extensions, m)
errors := append(parseErrors, validationErrors...)
// Initially, we will only log member extension errors.
if len(errors) > 0 {
errorPrefix := fmt.Sprintf("[%s] %s:", parent.String(), m.String())
for _, e := range errors {
klog.V(2).Infof("%s %s\n", errorPrefix, e)
}
}
g.emitExtensions(extensions)
return nil
}
func (g openAPITypeWriter) emitExtensions(extensions []extension) {
// If any extensions exist, then emit code to create them.
if len(extensions) == 0 {
return
}
g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil)
for _, extension := range extensions {
g.Do("\"$.$\": ", extension.xName)
if extension.hasMultipleValues() {
g.Do("[]string{\n", nil)
}
for _, value := range extension.values {
g.Do("\"$.$\",\n", value)
}
if extension.hasMultipleValues() {
g.Do("},\n", nil)
}
}
g.Do("},\n},\n", nil)
}
// TODO(#44005): Move this validation outside of this generator (probably to policy verifier)
func (g openAPITypeWriter) validatePatchTags(m *types.Member, parent *types.Type) error {
// TODO: Remove patch struct tag validation because they we are now consuming OpenAPI on server.
for _, tagKey := range tempPatchTags {
structTagValue := reflect.StructTag(m.Tags).Get(tagKey)
commentTagValue, err := getSingleTagsValue(m.CommentLines, tagKey)
if err != nil {
return err
}
if structTagValue != commentTagValue {
return fmt.Errorf("Tags in comment and struct should match for member (%s) of (%s)",
m.Name, parent.Name.String())
}
}
return nil
}
func (g openAPITypeWriter) generateDescription(CommentLines []string) {
var buffer bytes.Buffer
delPrevChar := func() {
if buffer.Len() > 0 {
buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n"
}
}
for _, line := range CommentLines {
// Ignore all lines after ---
if line == "---" {
break
}
line = strings.TrimRight(line, " ")
leading := strings.TrimLeft(line, " ")
switch {
case len(line) == 0: // Keep paragraphs
delPrevChar()
buffer.WriteString("\n\n")
case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs
case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl
default:
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
delPrevChar()
line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-someting..."
} else {
line += " "
}
buffer.WriteString(line)
}
}
postDoc := strings.TrimRight(buffer.String(), "\n")
postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to "
postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape "
postDoc = strings.Replace(postDoc, "\n", "\\n", -1)
postDoc = strings.Replace(postDoc, "\t", "\\t", -1)
postDoc = strings.Trim(postDoc, " ")
if postDoc != "" {
g.Do("Description: \"$.$\",\n", postDoc)
}
}
func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) error {
name := getReferableName(m)
if name == "" {
return nil
}
if err := g.validatePatchTags(m, parent); err != nil {
return err
}
g.Do("\"$.$\": {\n", name)
if err := g.generateMemberExtensions(m, parent); err != nil {
return err
}
g.Do("SchemaProps: spec.SchemaProps{\n", nil)
g.generateDescription(m.CommentLines)
jsonTags := getJsonTags(m)
if len(jsonTags) > 1 && jsonTags[1] == "string" {
g.generateSimpleProperty("string", "")
g.Do("},\n},\n", nil)
return nil
}
t := resolveAliasAndPtrType(m.Type)
// If we can get a openAPI type and format for this type, we consider it to be simple property
typeString, format := openapi.GetOpenAPITypeFormat(t.String())
if typeString != "" {
g.generateSimpleProperty(typeString, format)
g.Do("},\n},\n", nil)
return nil
}
switch t.Kind {
case types.Builtin:
return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", t)
case types.Map:
if err := g.generateMapProperty(t); err != nil {
return err
}
case types.Slice, types.Array:
if err := g.generateSliceProperty(t); err != nil {
return err
}
case types.Struct, types.Interface:
g.generateReferenceProperty(t)
default:
return fmt.Errorf("cannot generate spec for type %v", t)
}
g.Do("},\n},\n", nil)
return g.Error()
}
func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) {
g.Do("Type: []string{\"$.$\"},\n", typeString)
g.Do("Format: \"$.$\",\n", format)
}
func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) {
g.refTypes[t.Name.String()] = t
g.Do("Ref: ref(\"$.$\"),\n", t.Name.String())
}
func resolveAliasAndPtrType(t *types.Type) *types.Type {
var prev *types.Type
for prev != t {
prev = t
if t.Kind == types.Alias {
t = t.Underlying
}
if t.Kind == types.Pointer {
t = t.Elem
}
}
return t
}
func (g openAPITypeWriter) generateMapProperty(t *types.Type) error {
keyType := resolveAliasAndPtrType(t.Key)
elemType := resolveAliasAndPtrType(t.Elem)
// According to OpenAPI examples, only map from string is supported
if keyType.Name.Name != "string" {
return fmt.Errorf("map with non-string keys are not supported by OpenAPI in %v", t)
}
g.Do("Type: []string{\"object\"},\n", nil)
g.Do("AdditionalProperties: &spec.SchemaOrBool{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil)
typeString, format := openapi.GetOpenAPITypeFormat(elemType.String())
if typeString != "" {
g.generateSimpleProperty(typeString, format)
g.Do("},\n},\n},\n", nil)
return nil
}
switch elemType.Kind {
case types.Builtin:
return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType)
case types.Struct:
g.generateReferenceProperty(elemType)
case types.Slice, types.Array:
g.generateSliceProperty(elemType)
default:
return fmt.Errorf("map Element kind %v is not supported in %v", elemType.Kind, t.Name)
}
g.Do("},\n},\n},\n", nil)
return nil
}
func (g openAPITypeWriter) generateSliceProperty(t *types.Type) error {
elemType := resolveAliasAndPtrType(t.Elem)
g.Do("Type: []string{\"array\"},\n", nil)
g.Do("Items: &spec.SchemaOrArray{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil)
typeString, format := openapi.GetOpenAPITypeFormat(elemType.String())
if typeString != "" {
g.generateSimpleProperty(typeString, format)
g.Do("},\n},\n},\n", nil)
return nil
}
switch elemType.Kind {
case types.Builtin:
return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType)
case types.Struct:
g.generateReferenceProperty(elemType)
case types.Slice, types.Array:
g.generateSliceProperty(elemType)
default:
return fmt.Errorf("slice Element kind %v is not supported in %v", elemType.Kind, t)
}
g.Do("},\n},\n},\n", nil)
return nil
}

View File

@@ -1,625 +0,0 @@
/*
Copyright 2016 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 generators
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/gengo/generator"
"k8s.io/gengo/namer"
"k8s.io/gengo/parser"
"k8s.io/gengo/types"
)
func construct(t *testing.T, files map[string]string, testNamer namer.Namer) (*parser.Builder, types.Universe, []*types.Type) {
b := parser.New()
for name, src := range files {
if err := b.AddFileForTest(filepath.Dir(name), name, []byte(src)); err != nil {
t.Fatal(err)
}
}
u, err := b.FindTypes()
if err != nil {
t.Fatal(err)
}
orderer := namer.Orderer{Namer: testNamer}
o := orderer.OrderUniverse(u)
return b, u, o
}
func testOpenAPITypeWriter(t *testing.T, code string) (error, error, *assert.Assertions, *bytes.Buffer, *bytes.Buffer) {
assert := assert.New(t)
var testFiles = map[string]string{
"base/foo/bar.go": code,
}
rawNamer := namer.NewRawNamer("o", nil)
namers := namer.NameSystems{
"raw": namer.NewRawNamer("", nil),
"private": &namer.NameStrategy{
Join: func(pre string, in []string, post string) string {
return strings.Join(in, "_")
},
PrependPackageNames: 4, // enough to fully qualify from k8s.io/api/...
},
}
builder, universe, _ := construct(t, testFiles, rawNamer)
context, err := generator.NewContext(builder, namers, "raw")
if err != nil {
t.Fatal(err)
}
blahT := universe.Type(types.Name{Package: "base/foo", Name: "Blah"})
callBuffer := &bytes.Buffer{}
callSW := generator.NewSnippetWriter(callBuffer, context, "$", "$")
callError := newOpenAPITypeWriter(callSW).generateCall(blahT)
funcBuffer := &bytes.Buffer{}
funcSW := generator.NewSnippetWriter(funcBuffer, context, "$", "$")
funcError := newOpenAPITypeWriter(funcSW).generate(blahT)
return callError, funcError, assert, callBuffer, funcBuffer
}
func TestSimple(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
// Blah is a test.
// +k8s:openapi-gen=true
// +k8s:openapi-gen=x-kubernetes-type-tag:type_test
type Blah struct {
// A simple string
String string
// A simple int
Int int `+"`"+`json:",omitempty"`+"`"+`
// An int considered string simple int
IntString int `+"`"+`json:",string"`+"`"+`
// A simple int64
Int64 int64
// A simple int32
Int32 int32
// A simple int16
Int16 int16
// A simple int8
Int8 int8
// A simple int
Uint uint
// A simple int64
Uint64 uint64
// A simple int32
Uint32 uint32
// A simple int16
Uint16 uint16
// A simple int8
Uint8 uint8
// A simple byte
Byte byte
// A simple boolean
Bool bool
// A simple float64
Float64 float64
// A simple float32
Float32 float32
// a base64 encoded characters
ByteArray []byte
// a member with an extension
// +k8s:openapi-gen=x-kubernetes-member-tag:member_test
WithExtension string
// a member with struct tag as extension
// +patchStrategy=merge
// +patchMergeKey=pmk
WithStructTagExtension string `+"`"+`patchStrategy:"merge" patchMergeKey:"pmk"`+"`"+`
// a member with a list type
// +listType=atomic
WithListType []string
}
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": schema_base_foo_Blah(ref),
`, callBuffer.String())
assert.Equal(`func schema_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Blah is a test.",
Properties: map[string]spec.Schema{
"String": {
SchemaProps: spec.SchemaProps{
Description: "A simple string",
Type: []string{"string"},
Format: "",
},
},
"Int64": {
SchemaProps: spec.SchemaProps{
Description: "A simple int64",
Type: []string{"integer"},
Format: "int64",
},
},
"Int32": {
SchemaProps: spec.SchemaProps{
Description: "A simple int32",
Type: []string{"integer"},
Format: "int32",
},
},
"Int16": {
SchemaProps: spec.SchemaProps{
Description: "A simple int16",
Type: []string{"integer"},
Format: "int32",
},
},
"Int8": {
SchemaProps: spec.SchemaProps{
Description: "A simple int8",
Type: []string{"integer"},
Format: "byte",
},
},
"Uint": {
SchemaProps: spec.SchemaProps{
Description: "A simple int",
Type: []string{"integer"},
Format: "int32",
},
},
"Uint64": {
SchemaProps: spec.SchemaProps{
Description: "A simple int64",
Type: []string{"integer"},
Format: "int64",
},
},
"Uint32": {
SchemaProps: spec.SchemaProps{
Description: "A simple int32",
Type: []string{"integer"},
Format: "int64",
},
},
"Uint16": {
SchemaProps: spec.SchemaProps{
Description: "A simple int16",
Type: []string{"integer"},
Format: "int32",
},
},
"Uint8": {
SchemaProps: spec.SchemaProps{
Description: "A simple int8",
Type: []string{"integer"},
Format: "byte",
},
},
"Byte": {
SchemaProps: spec.SchemaProps{
Description: "A simple byte",
Type: []string{"integer"},
Format: "byte",
},
},
"Bool": {
SchemaProps: spec.SchemaProps{
Description: "A simple boolean",
Type: []string{"boolean"},
Format: "",
},
},
"Float64": {
SchemaProps: spec.SchemaProps{
Description: "A simple float64",
Type: []string{"number"},
Format: "double",
},
},
"Float32": {
SchemaProps: spec.SchemaProps{
Description: "A simple float32",
Type: []string{"number"},
Format: "float",
},
},
"ByteArray": {
SchemaProps: spec.SchemaProps{
Description: "a base64 encoded characters",
Type: []string{"string"},
Format: "byte",
},
},
"WithExtension": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-member-tag": "member_test",
},
},
SchemaProps: spec.SchemaProps{
Description: "a member with an extension",
Type: []string{"string"},
Format: "",
},
},
"WithStructTagExtension": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-patch-merge-key": "pmk",
"x-kubernetes-patch-strategy": "merge",
},
},
SchemaProps: spec.SchemaProps{
Description: "a member with struct tag as extension",
Type: []string{"string"},
Format: "",
},
},
"WithListType": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-type": "atomic",
},
},
SchemaProps: spec.SchemaProps{
Description: "a member with a list type",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"String","Int64","Int32","Int16","Int8","Uint","Uint64","Uint32","Uint16","Uint8","Byte","Bool","Float64","Float32","ByteArray","WithExtension","WithStructTagExtension","WithListType"},
},
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-type-tag": "type_test",
},
},
},
Dependencies: []string{
},
}
}
`, funcBuffer.String())
}
func TestFailingSample1(t *testing.T) {
_, funcErr, assert, _, _ := testOpenAPITypeWriter(t, `
package foo
// Map sample tests openAPIGen.generateMapProperty method.
type Blah struct {
// A sample String to String map
StringToArray map[string]map[string]string
}
`)
if assert.Error(funcErr, "An error was expected") {
assert.Equal(funcErr, fmt.Errorf("map Element kind Map is not supported in map[string]map[string]string"))
}
}
func TestFailingSample2(t *testing.T) {
_, funcErr, assert, _, _ := testOpenAPITypeWriter(t, `
package foo
// Map sample tests openAPIGen.generateMapProperty method.
type Blah struct {
// A sample String to String map
StringToArray map[int]string
} `)
if assert.Error(funcErr, "An error was expected") {
assert.Equal(funcErr, fmt.Errorf("map with non-string keys are not supported by OpenAPI in map[int]string"))
}
}
func TestCustomDef(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
import openapi "k8s.io/kube-openapi/pkg/common"
type Blah struct {
}
func (_ Blah) OpenAPIDefinition() openapi.OpenAPIDefinition {
return openapi.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
},
},
}
}
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": foo.Blah{}.OpenAPIDefinition(),
`, callBuffer.String())
assert.Equal(``, funcBuffer.String())
}
func TestCustomDefs(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
// Blah is a custom type
type Blah struct {
}
func (_ Blah) OpenAPISchemaType() []string { return []string{"string"} }
func (_ Blah) OpenAPISchemaFormat() string { return "date-time" }
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": schema_base_foo_Blah(ref),
`, callBuffer.String())
assert.Equal(`func schema_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Blah is a custom type",
Type:foo.Blah{}.OpenAPISchemaType(),
Format:foo.Blah{}.OpenAPISchemaFormat(),
},
},
}
}
`, funcBuffer.String())
}
func TestPointer(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
// PointerSample demonstrate pointer's properties
type Blah struct {
// A string pointer
StringPointer *string
// A struct pointer
StructPointer *Blah
// A slice pointer
SlicePointer *[]string
// A map pointer
MapPointer *map[string]string
}
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": schema_base_foo_Blah(ref),
`, callBuffer.String())
assert.Equal(`func schema_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "PointerSample demonstrate pointer's properties",
Properties: map[string]spec.Schema{
"StringPointer": {
SchemaProps: spec.SchemaProps{
Description: "A string pointer",
Type: []string{"string"},
Format: "",
},
},
"StructPointer": {
SchemaProps: spec.SchemaProps{
Description: "A struct pointer",
Ref: ref("base/foo.Blah"),
},
},
"SlicePointer": {
SchemaProps: spec.SchemaProps{
Description: "A slice pointer",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
"MapPointer": {
SchemaProps: spec.SchemaProps{
Description: "A map pointer",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"StringPointer","StructPointer","SlicePointer","MapPointer"},
},
},
Dependencies: []string{
"base/foo.Blah",},
}
}
`, funcBuffer.String())
}
func TestNestedLists(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
// Blah is a test.
// +k8s:openapi-gen=true
// +k8s:openapi-gen=x-kubernetes-type-tag:type_test
type Blah struct {
// Nested list
NestedList [][]int64
}
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": schema_base_foo_Blah(ref),
`, callBuffer.String())
assert.Equal(`func schema_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Blah is a test.",
Properties: map[string]spec.Schema{
"NestedList": {
SchemaProps: spec.SchemaProps{
Description: "Nested list",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
Format: "int64",
},
},
},
},
},
},
},
},
},
Required: []string{"NestedList"},
},
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-type-tag": "type_test",
},
},
},
Dependencies: []string{
},
}
}
`, funcBuffer.String())
}
func TestExtensions(t *testing.T) {
callErr, funcErr, assert, callBuffer, funcBuffer := testOpenAPITypeWriter(t, `
package foo
// Blah is a test.
// +k8s:openapi-gen=true
// +k8s:openapi-gen=x-kubernetes-type-tag:type_test
type Blah struct {
// a member with a list type
// +listType=map
// +listMapKey=port
// +listMapKey=protocol
WithListField []string
}
`)
if callErr != nil {
t.Fatal(callErr)
}
if funcErr != nil {
t.Fatal(funcErr)
}
assert.Equal(`"base/foo.Blah": schema_base_foo_Blah(ref),
`, callBuffer.String())
assert.Equal(`func schema_base_foo_Blah(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Blah is a test.",
Properties: map[string]spec.Schema{
"WithListField": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-list-map-keys": []string{
"port",
"protocol",
},
"x-kubernetes-list-type": "map",
},
},
SchemaProps: spec.SchemaProps{
Description: "a member with a list type",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"WithListField"},
},
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-type-tag": "type_test",
},
},
},
Dependencies: []string{
},
}
}
`, funcBuffer.String())
}

View File

@@ -1,4 +0,0 @@
reviewers:
- roycaihw
approvers:
- roycaihw

View File

@@ -1,23 +0,0 @@
/*
Copyright 2018 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 rules contains API rules that are enforced in OpenAPI spec generation
// as part of the machinery. Files under this package implement APIRule interface
// which evaluates Go type and produces list of API rule violations.
//
// Implementations of APIRule should be added to API linter under openAPIGen code-
// generator to get integrated in the generation process.
package rules

View File

@@ -1,172 +0,0 @@
/*
Copyright 2018 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 rules
import (
"reflect"
"strings"
"k8s.io/kube-openapi/pkg/util/sets"
"k8s.io/gengo/types"
)
var (
// Blacklist of JSON tags that should skip match evaluation
jsonTagBlacklist = sets.NewString(
// Omitted field is ignored by the package
"-",
)
// Blacklist of JSON names that should skip match evaluation
jsonNameBlacklist = sets.NewString(
// Empty name is used for inline struct field (e.g. metav1.TypeMeta)
"",
// Special case for object and list meta
"metadata",
)
// List of substrings that aren't allowed in Go name and JSON name
disallowedNameSubstrings = sets.NewString(
// Underscore is not allowed in either name
"_",
// Dash is not allowed in either name. Note that since dash is a valid JSON tag, this should be checked
// after JSON tag blacklist check.
"-",
)
)
/*
NamesMatch implements APIRule interface.
Go field names must be CamelCase. JSON field names must be camelCase. Other than capitalization of the
initial letter, the two should almost always match. No underscores nor dashes in either.
This rule verifies the convention "Other than capitalization of the initial letter, the two should almost always match."
Examples (also in unit test):
Go name | JSON name | match
podSpec false
PodSpec podSpec true
PodSpec PodSpec false
podSpec podSpec false
PodSpec spec false
Spec podSpec false
JSONSpec jsonSpec true
JSONSpec jsonspec false
HTTPJSONSpec httpJSONSpec true
NOTE: this validator cannot tell two sequential all-capital words from one word, therefore the case below
is also considered matched.
HTTPJSONSpec httpjsonSpec true
NOTE: JSON names in jsonNameBlacklist should skip evaluation
true
podSpec true
podSpec - true
podSpec metadata true
*/
type NamesMatch struct{}
// Name returns the name of APIRule
func (n *NamesMatch) Name() string {
return "names_match"
}
// Validate evaluates API rule on type t and returns a list of field names in
// the type that violate the rule. Empty field name [""] implies the entire
// type violates the rule.
func (n *NamesMatch) Validate(t *types.Type) ([]string, error) {
fields := make([]string, 0)
// Only validate struct type and ignore the rest
switch t.Kind {
case types.Struct:
for _, m := range t.Members {
goName := m.Name
jsonTag, ok := reflect.StructTag(m.Tags).Lookup("json")
// Distinguish empty JSON tag and missing JSON tag. Empty JSON tag / name is
// allowed (in JSON name blacklist) but missing JSON tag is invalid.
if !ok {
fields = append(fields, goName)
continue
}
if jsonTagBlacklist.Has(jsonTag) {
continue
}
jsonName := strings.Split(jsonTag, ",")[0]
if !namesMatch(goName, jsonName) {
fields = append(fields, goName)
}
}
}
return fields, nil
}
// namesMatch evaluates if goName and jsonName match the API rule
// TODO: Use an off-the-shelf CamelCase solution instead of implementing this logic. The following existing
// packages have been tried out:
// github.com/markbates/inflect
// github.com/segmentio/go-camelcase
// github.com/iancoleman/strcase
// github.com/fatih/camelcase
// Please see https://github.com/kubernetes/kube-openapi/pull/83#issuecomment-400842314 for more details
// about why they don't satisfy our need. What we need can be a function that detects an acronym at the
// beginning of a string.
func namesMatch(goName, jsonName string) bool {
if jsonNameBlacklist.Has(jsonName) {
return true
}
if !isAllowedName(goName) || !isAllowedName(jsonName) {
return false
}
if strings.ToLower(goName) != strings.ToLower(jsonName) {
return false
}
// Go field names must be CamelCase. JSON field names must be camelCase.
if !isCapital(goName[0]) || isCapital(jsonName[0]) {
return false
}
for i := 0; i < len(goName); i++ {
if goName[i] == jsonName[i] {
// goName[0:i-1] is uppercase and jsonName[0:i-1] is lowercase, goName[i:]
// and jsonName[i:] should match;
// goName[i] should be lowercase if i is equal to 1, e.g.:
// goName | jsonName
// PodSpec podSpec
// or uppercase if i is greater than 1, e.g.:
// goname | jsonName
// JSONSpec jsonSpec
// This is to rule out cases like:
// goname | jsonName
// JSONSpec jsonspec
return goName[i:] == jsonName[i:] && (i == 1 || isCapital(goName[i]))
}
}
return true
}
// isCaptical returns true if one character is capital
func isCapital(b byte) bool {
return b >= 'A' && b <= 'Z'
}
// isAllowedName checks the list of disallowedNameSubstrings and returns true if name doesn't contain
// any disallowed substring.
func isAllowedName(name string) bool {
for _, substr := range disallowedNameSubstrings.UnsortedList() {
if strings.Contains(name, substr) {
return false
}
}
return true
}

View File

@@ -1,359 +0,0 @@
/*
Copyright 2018 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 rules
import (
"reflect"
"testing"
"k8s.io/gengo/types"
)
func TestNamesMatch(t *testing.T) {
tcs := []struct {
// name of test case
name string
t *types.Type
// expected list of violation fields
expected []string
}{
// The comments are in format of {goName, jsonName, match},
// {"PodSpec", "podSpec", true},
{
name: "simple",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec"`,
},
},
},
expected: []string{},
},
// {"PodSpec", "podSpec", true},
{
name: "multiple_json_tags",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec,omitempty"`,
},
},
},
expected: []string{},
},
// {"PodSpec", "podSpec", true},
{
name: "protobuf_tag",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec,omitempty" protobuf:"bytes,1,opt,name=podSpec"`,
},
},
},
expected: []string{},
},
// {"", "podSpec", false},
{
name: "empty",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "",
Tags: `json:"podSpec"`,
},
},
},
expected: []string{""},
},
// {"PodSpec", "PodSpec", false},
{
name: "CamelCase_CamelCase",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"PodSpec"`,
},
},
},
expected: []string{"PodSpec"},
},
// {"podSpec", "podSpec", false},
{
name: "camelCase_camelCase",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "podSpec",
Tags: `json:"podSpec"`,
},
},
},
expected: []string{"podSpec"},
},
// {"PodSpec", "spec", false},
{
name: "short_json_name",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"spec"`,
},
},
},
expected: []string{"PodSpec"},
},
// {"Spec", "podSpec", false},
{
name: "long_json_name",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "Spec",
Tags: `json:"podSpec"`,
},
},
},
expected: []string{"Spec"},
},
// {"JSONSpec", "jsonSpec", true},
{
name: "acronym",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "JSONSpec",
Tags: `json:"jsonSpec"`,
},
},
},
expected: []string{},
},
// {"JSONSpec", "jsonspec", false},
{
name: "acronym_invalid",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "JSONSpec",
Tags: `json:"jsonspec"`,
},
},
},
expected: []string{"JSONSpec"},
},
// {"HTTPJSONSpec", "httpJSONSpec", true},
{
name: "multiple_acronym",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "HTTPJSONSpec",
Tags: `json:"httpJSONSpec"`,
},
},
},
expected: []string{},
},
// // NOTE: this validator cannot tell two sequential all-capital words from one word,
// // therefore the case below is also considered matched.
// {"HTTPJSONSpec", "httpjsonSpec", true},
{
name: "multiple_acronym_as_one",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "HTTPJSONSpec",
Tags: `json:"httpjsonSpec"`,
},
},
},
expected: []string{},
},
// NOTE: JSON tags in jsonTagBlacklist should skip evaluation
{
name: "blacklist_tag_dash",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "podSpec",
Tags: `json:"-"`,
},
},
},
expected: []string{},
},
// {"PodSpec", "-", false},
{
name: "invalid_json_name_dash",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"-,"`,
},
},
},
expected: []string{"PodSpec"},
},
// NOTE: JSON names in jsonNameBlacklist should skip evaluation
// {"", "", true},
{
name: "unspecified",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "",
Tags: `json:""`,
},
},
},
expected: []string{},
},
// {"podSpec", "", true},
{
name: "blacklist_empty",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "podSpec",
Tags: `json:""`,
},
},
},
expected: []string{},
},
// {"podSpec", "metadata", true},
{
name: "blacklist_metadata",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "podSpec",
Tags: `json:"metadata"`,
},
},
},
expected: []string{},
},
{
name: "non_struct",
t: &types.Type{
Kind: types.Map,
},
expected: []string{},
},
{
name: "no_json_tag",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `podSpec`,
},
},
},
expected: []string{"PodSpec"},
},
// NOTE: this is to expand test coverage
// {"S", "s", true},
{
name: "single_character",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "S",
Tags: `json:"s"`,
},
},
},
expected: []string{},
},
// NOTE: names with disallowed substrings should fail evaluation
// {"Pod-Spec", "pod-Spec", false},
{
name: "disallowed_substring_dash",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "Pod-Spec",
Tags: `json:"pod-Spec"`,
},
},
},
expected: []string{"Pod-Spec"},
},
// {"Pod_Spec", "pod_Spec", false},
{
name: "disallowed_substring_underscore",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "Pod_Spec",
Tags: `json:"pod_Spec"`,
},
},
},
expected: []string{"Pod_Spec"},
},
}
n := &NamesMatch{}
for _, tc := range tcs {
if violations, _ := n.Validate(tc.t); !reflect.DeepEqual(violations, tc.expected) {
t.Errorf("unexpected validation result: test name %v, want: %v, got: %v",
tc.name, tc.expected, violations)
}
}
}
// TestRuleName tests the Name of API rule. This is to expand test coverage
func TestRuleName(t *testing.T) {
ruleName := "names_match"
n := &NamesMatch{}
if n.Name() != ruleName {
t.Errorf("unexpected API rule name: want: %v, got: %v", ruleName, n.Name())
}
}

View File

@@ -1,64 +0,0 @@
/*
Copyright 2018 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 rules
import (
"reflect"
"strings"
"k8s.io/gengo/types"
)
// OmitEmptyMatchCase implements APIRule interface.
// "omitempty" must appear verbatim (no case variants).
type OmitEmptyMatchCase struct{}
func (n *OmitEmptyMatchCase) Name() string {
return "omitempty_match_case"
}
func (n *OmitEmptyMatchCase) Validate(t *types.Type) ([]string, error) {
fields := make([]string, 0)
// Only validate struct type and ignore the rest
switch t.Kind {
case types.Struct:
for _, m := range t.Members {
goName := m.Name
jsonTag, ok := reflect.StructTag(m.Tags).Lookup("json")
if !ok {
continue
}
parts := strings.Split(jsonTag, ",")
if len(parts) < 2 {
// no tags other than name
continue
}
if parts[0] == "-" {
// not serialized
continue
}
for _, part := range parts[1:] {
if strings.EqualFold(part, "omitempty") && part != "omitempty" {
fields = append(fields, goName)
}
}
}
}
return fields, nil
}

View File

@@ -1,110 +0,0 @@
/*
Copyright 2018 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 rules
import (
"reflect"
"testing"
"k8s.io/gengo/types"
)
func TestOmitEmptyMatchCase(t *testing.T) {
tcs := []struct {
// name of test case
name string
t *types.Type
// expected list of violation fields
expected []string
}{
{
name: "simple",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec"`,
},
},
},
expected: []string{},
},
{
name: "unserialized",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"-,inline"`,
},
},
},
expected: []string{},
},
{
name: "named_omitEmpty",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "OmitEmpty",
Tags: `json:"omitEmpty,inline"`,
},
},
},
expected: []string{},
},
{
name: "valid",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec,omitempty"`,
},
},
},
expected: []string{},
},
{
name: "invalid",
t: &types.Type{
Kind: types.Struct,
Members: []types.Member{
types.Member{
Name: "PodSpec",
Tags: `json:"podSpec,omitEmpty"`,
},
},
},
expected: []string{"PodSpec"},
},
}
n := &OmitEmptyMatchCase{}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
if violations, _ := n.Validate(tc.t); !reflect.DeepEqual(violations, tc.expected) {
t.Errorf("unexpected validation result: want: %v, got: %v", tc.expected, violations)
}
})
}
}