k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/rules/names_match.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package rules 18 19 import ( 20 "reflect" 21 "strings" 22 23 "k8s.io/kube-openapi/pkg/util/sets" 24 25 "k8s.io/gengo/v2/types" 26 ) 27 28 var ( 29 // Blacklist of JSON tags that should skip match evaluation 30 jsonTagBlacklist = sets.NewString( 31 // Omitted field is ignored by the package 32 "-", 33 ) 34 35 // Blacklist of JSON names that should skip match evaluation 36 jsonNameBlacklist = sets.NewString( 37 // Empty name is used for inline struct field (e.g. metav1.TypeMeta) 38 "", 39 // Special case for object and list meta 40 "metadata", 41 ) 42 43 // List of substrings that aren't allowed in Go name and JSON name 44 disallowedNameSubstrings = sets.NewString( 45 // Underscore is not allowed in either name 46 "_", 47 // Dash is not allowed in either name. Note that since dash is a valid JSON tag, this should be checked 48 // after JSON tag blacklist check. 49 "-", 50 ) 51 ) 52 53 /* 54 NamesMatch implements APIRule interface. 55 Go field names must be CamelCase. JSON field names must be camelCase. Other than capitalization of the 56 initial letter, the two should almost always match. No underscores nor dashes in either. 57 This rule verifies the convention "Other than capitalization of the initial letter, the two should almost always match." 58 Examples (also in unit test): 59 60 Go name | JSON name | match 61 podSpec false 62 PodSpec podSpec true 63 PodSpec PodSpec false 64 podSpec podSpec false 65 PodSpec spec false 66 Spec podSpec false 67 JSONSpec jsonSpec true 68 JSONSpec jsonspec false 69 HTTPJSONSpec httpJSONSpec true 70 71 NOTE: this validator cannot tell two sequential all-capital words from one word, therefore the case below 72 is also considered matched. 73 74 HTTPJSONSpec httpjsonSpec true 75 76 NOTE: JSON names in jsonNameBlacklist should skip evaluation 77 78 true 79 podSpec true 80 podSpec - true 81 podSpec metadata true 82 */ 83 type NamesMatch struct{} 84 85 // Name returns the name of APIRule 86 func (n *NamesMatch) Name() string { 87 return "names_match" 88 } 89 90 // Validate evaluates API rule on type t and returns a list of field names in 91 // the type that violate the rule. Empty field name [""] implies the entire 92 // type violates the rule. 93 func (n *NamesMatch) Validate(t *types.Type) ([]string, error) { 94 fields := make([]string, 0) 95 96 // Only validate struct type and ignore the rest 97 switch t.Kind { 98 case types.Struct: 99 for _, m := range t.Members { 100 goName := m.Name 101 jsonTag, ok := reflect.StructTag(m.Tags).Lookup("json") 102 // Distinguish empty JSON tag and missing JSON tag. Empty JSON tag / name is 103 // allowed (in JSON name blacklist) but missing JSON tag is invalid. 104 if !ok { 105 fields = append(fields, goName) 106 continue 107 } 108 if jsonTagBlacklist.Has(jsonTag) { 109 continue 110 } 111 jsonName := strings.Split(jsonTag, ",")[0] 112 if !namesMatch(goName, jsonName) { 113 fields = append(fields, goName) 114 } 115 } 116 } 117 return fields, nil 118 } 119 120 // namesMatch evaluates if goName and jsonName match the API rule 121 // TODO: Use an off-the-shelf CamelCase solution instead of implementing this logic. The following existing 122 // 123 // packages have been tried out: 124 // github.com/markbates/inflect 125 // github.com/segmentio/go-camelcase 126 // github.com/iancoleman/strcase 127 // github.com/fatih/camelcase 128 // Please see https://github.com/kubernetes/kube-openapi/pull/83#issuecomment-400842314 for more details 129 // about why they don't satisfy our need. What we need can be a function that detects an acronym at the 130 // beginning of a string. 131 func namesMatch(goName, jsonName string) bool { 132 if jsonNameBlacklist.Has(jsonName) { 133 return true 134 } 135 if !isAllowedName(goName) || !isAllowedName(jsonName) { 136 return false 137 } 138 if !strings.EqualFold(goName, jsonName) { 139 return false 140 } 141 // Go field names must be CamelCase. JSON field names must be camelCase. 142 if !isCapital(goName[0]) || isCapital(jsonName[0]) { 143 return false 144 } 145 for i := 0; i < len(goName); i++ { 146 if goName[i] == jsonName[i] { 147 // goName[0:i-1] is uppercase and jsonName[0:i-1] is lowercase, goName[i:] 148 // and jsonName[i:] should match; 149 // goName[i] should be lowercase if i is equal to 1, e.g.: 150 // goName | jsonName 151 // PodSpec podSpec 152 // or uppercase if i is greater than 1, e.g.: 153 // goname | jsonName 154 // JSONSpec jsonSpec 155 // This is to rule out cases like: 156 // goname | jsonName 157 // JSONSpec jsonspec 158 return goName[i:] == jsonName[i:] && (i == 1 || isCapital(goName[i])) 159 } 160 } 161 return true 162 } 163 164 // isCapital returns true if one character is capital 165 func isCapital(b byte) bool { 166 return b >= 'A' && b <= 'Z' 167 } 168 169 // isAllowedName checks the list of disallowedNameSubstrings and returns true if name doesn't contain 170 // any disallowed substring. 171 func isAllowedName(name string) bool { 172 for _, substr := range disallowedNameSubstrings.UnsortedList() { 173 if strings.Contains(name, substr) { 174 return false 175 } 176 } 177 return true 178 }