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  }