codegen/casing.go (165 lines of code) (raw):
// Copyright (c) 2023 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
package codegen
import (
"bytes"
"regexp"
"strings"
"sync"
"unicode"
"unicode/utf8"
)
var pascalCaseMap *sync.Map
// CommonInitialisms is taken from https://github.com/golang/lint/blob/206c0f020eba0f7fbcfbc467a5eb808037df2ed6/lint.go#L731
var CommonInitialisms = map[string]bool{
"ACL": true,
"API": true,
"ASCII": true,
"CPU": true,
"CSS": true,
"DNS": true,
"EOF": true,
"GUID": true,
"HTML": true,
"HTTP": true,
"HTTPS": true,
"ID": true,
"IP": true,
"JSON": true,
"LHS": true,
"OS": true,
"QPS": true,
"RAM": true,
"RHS": true,
"RPC": true,
"SLA": true,
"SMTP": true,
"SQL": true,
"SSH": true,
"TCP": true,
"TLS": true,
"TTL": true,
"UDP": true,
"UI": true,
"UID": true,
"UUID": true,
"URI": true,
"URL": true,
"UTF8": true,
"VM": true,
"XML": true,
"XMPP": true,
"XSRF": true,
"XSS": true,
}
var camelingRegex = regexp.MustCompile("[0-9A-Za-z]+")
// LintAcronym correct the naming for initialisms
func LintAcronym(key string) string {
key = PascalCase(key)
for k := range CommonInitialisms {
initial := string(k[0]) + strings.ToLower(k[1:])
if strings.Contains(key, initial) {
key = strings.Replace(key, initial, k, -1)
}
}
return key
}
// startsWithInitialism returns the initialism if the given string begins with it
func startsWithInitialism(s string) string {
var initialism string
// the longest initialism is 5 char, the shortest 2
for i := 1; i <= 5; i++ {
if len(s) > i-1 && CommonInitialisms[s[:i]] {
initialism = s[:i]
}
}
return initialism
}
// CamelCase converts the given string to camel case
func CamelCase(src string) string {
byteSrc := []byte(src)
chunks := camelingRegex.FindAll(byteSrc, -1)
for idx, val := range chunks {
if idx > 0 {
chunks[idx] = ensureGolangAncronymCasing(bytes.Title(val))
} else {
chunks[idx][0] = bytes.ToLower(val[0:1])[0]
}
}
return string(bytes.Join(chunks, nil))
}
func packageName(src string) string {
byteSrc := []byte(src)
chunks := camelingRegex.FindAll(byteSrc, -1)
for idx, val := range chunks {
chunks[idx] = bytes.ToLower(val)
}
return string(bytes.Join(chunks, nil))
}
func ensureGolangAncronymCasing(segment []byte) []byte {
upper := bytes.ToUpper(segment)
if CommonInitialisms[string(upper)] {
return upper
}
if initial := startsWithInitialism(string(upper)); initial != "" {
return append([]byte(initial), segment[len(initial):]...)
}
return segment
}
// PascalCase converts the given string to pascal case
func PascalCase(src string) string {
if pascalCaseMap == nil {
pascalCaseMap = &sync.Map{}
}
if res, ok := pascalCaseMap.Load(src); ok {
return res.(string)
}
// borrow pascal casing logic from thriftrw-go since the two implementations
// must match otherwise the thriftrw-go generated field name does not match
// the RequestType/ResponseType we use in the endpoint/client templates.
// https://github.com/thriftrw/thriftrw-go/blob/1c52f516bdc5ca90dc090ba2a8ee0bd11bf04f96/gen/string.go#L48
words := strings.Split(src, "_")
res := pascalCase(len(words) == 1 /* all caps */, words...)
pascalCaseMap.Store(src, res)
return res
}
// pascalCase combines the given words using PascalCase.
//
// If allowAllCaps is true, when an all-caps word that is not a known
// abbreviation is encountered, it is left unchanged. Otherwise, it is
// Titlecased.
func pascalCase(allowAllCaps bool, words ...string) string {
for i, chunk := range words {
if len(chunk) == 0 {
// foo__bar
continue
}
// known initalism
init := strings.ToUpper(chunk)
if _, ok := CommonInitialisms[init]; ok {
words[i] = init
continue
}
// Was SCREAMING_SNAKE_CASE and not a known initialism so Titlecase it.
if isAllCaps(chunk) && !allowAllCaps {
// A single ALLCAPS word does not count as SCREAMING_SNAKE_CASE.
// There must be at least one underscore.
words[i] = strings.Title(strings.ToLower(chunk))
continue
}
// Just another word, but could already be camelCased somehow, so just
// change the first letter.
head, headIndex := utf8.DecodeRuneInString(chunk)
words[i] = string(unicode.ToUpper(head)) + string(chunk[headIndex:])
}
return strings.Join(words, "")
}
// isAllCaps checks if a string contains all capital letters only. Non-letters
// are not considered.
func isAllCaps(s string) bool {
for _, r := range s {
if unicode.IsLetter(r) && !unicode.IsUpper(r) {
return false
}
}
return true
}
// CamelToSnake converts a given string to snake case, based on
// https://github.com/serenize/snaker/blob/master/snaker.go
func CamelToSnake(s string) string {
var words []string
var lastPos int
rs := []rune(s)
for i := 0; i < len(rs); i++ {
if i > 0 && unicode.IsUpper(rs[i]) {
if initialism := startsWithInitialism(s[lastPos:]); initialism != "" {
words = append(words, initialism)
i += len(initialism) - 1
lastPos = i
continue
}
words = append(words, s[lastPos:i])
lastPos = i
}
}
// append the last word
if s[lastPos:] != "" {
words = append(words, s[lastPos:])
}
return strings.ToLower(strings.Join(words, "_"))
}
// LowerPascal is Pascal with first char lower
func LowerPascal(str string) string {
str = PascalCase(str)
return strings.ToLower(string(str[0])) + string(str[1:])
}