173 lines
5.5 KiB
Go
173 lines
5.5 KiB
Go
|
/*
|
||
|
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
|
||
|
}
|