Home

gen @main - refs - log -
-
https://git.jolheiser.com/gen.git
Generate Go flags and Nix module
gen / names.go
- raw -
  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
}