1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
package main
import (
"fmt"
"go/token"
"slices"
"strings"
"unicode"
)
// commonInitialisms are words that are fully upper-cased in Go names, e.g. "url" becomes "URL"
var commonInitialisms = []string{
"acl", "api", "ascii", "cpu", "css", "dns", "eof", "guid", "html", "http",
"https", "id", "ip", "json", "lhs", "qps", "ram", "rhs", "rpc", "sla",
"smtp", "sql", "ssh", "tcp", "tls", "ttl", "udp", "ui", "uid", "uuid",
"uri", "url", "utf8", "vm", "xml", "xmpp", "xsrf", "xss",
}
// namer turns CUE labels into Go identifiers
type namer struct {
initialisms map[string]bool
}
// newNamer returns a namer that knows the common initialisms, plus any extra ones
func newNamer(extra []string) namer {
initialisms := make(map[string]bool)
for _, word := range slices.Concat(commonInitialisms, extra) {
word = strings.ToLower(strings.TrimSpace(word))
if word != "" {
initialisms[word] = true
}
}
return namer{initialisms: initialisms}
}
// splitWords splits a label into lower-cased words.
//
// Any character that isn't a letter or digit separates words, as do camelCase boundaries:
//
// cloneURL -> clone, url
// URLPath -> url, path
// utf8Name -> utf8, name
func splitWords(s string) []string {
var words []string
var word []rune
endWord := func() {
if len(word) > 0 {
words = append(words, strings.ToLower(string(word)))
word = word[:0]
}
}
runes := []rune(s)
for idx, r := range runes {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
endWord()
continue
}
if unicode.IsUpper(r) && len(word) > 0 && startsWord(runes, idx) {
endWord()
}
word = append(word, r)
}
endWord()
return words
}
// startsWord reports whether the upper-case rune at runes[idx] starts a new word
func startsWord(runes []rune, idx int) bool {
prev := runes[idx-1]
if unicode.IsLower(prev) || unicode.IsDigit(prev) {
return true
}
// The last capital in a run starts a new word if it's followed by lower-case, e.g. the P in URLPath
nextIsLower := idx+1 < len(runes) && unicode.IsLower(runes[idx+1])
return unicode.IsUpper(prev) && nextIsLower
}
// capitalize upper-cases the first letter of word, or the whole word if it's an initialism
func (n namer) capitalize(word string) string {
if n.initialisms[word] {
return strings.ToUpper(word)
}
runes := []rune(word)
runes[0] = unicode.ToUpper(runes[0])
return string(runes)
}
// goName joins the words of label into a Go identifier, either PascalCase (exported) or camelCase
func (n namer) goName(label string, exported bool) (string, error) {
words := splitWords(label)
if len(words) == 0 {
return "", fmt.Errorf("cannot derive a Go name from label %q; set @go(name=...)", label)
}
var name strings.Builder
for idx, word := range words {
if idx == 0 && !exported {
name.WriteString(word)
continue
}
name.WriteString(n.capitalize(word))
}
if !token.IsIdentifier(name.String()) {
return "", fmt.Errorf("label %q gives %q, which is not a valid Go identifier; set @go(name=...)", label, name.String())
}
return name.String(), nil
}
// fieldName returns the exported Go struct field name for label
func (n namer) fieldName(label string) (string, error) {
return n.goName(label, true)
}
// typeName returns the unexported Go struct type name for label
func (n namer) typeName(label, suffix string) (string, error) {
name, err := n.goName(label, false)
if err != nil {
return "", err
}
name += suffix
if token.IsKeyword(name) {
return "", fmt.Errorf("label %q gives the Go keyword %q; set @go(type=...)", label, name)
}
return name, nil
}
|