Skip to content

Commit

Permalink
Merge pull request 99designs#579 from 99designs/fix-camelcase
Browse files Browse the repository at this point in the history
Take care about commonInitialisms in ToGo
  • Loading branch information
vektah authored Mar 6, 2019
2 parents 49d3179 + 855e41b commit 5444565
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 105 deletions.
6 changes: 3 additions & 3 deletions codegen/generated!.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ type DirectiveRoot struct {
type ComplexityRoot struct {
{{ range $object := .Objects }}
{{ if not $object.IsReserved -}}
{{ $object.Name|toCamel }} struct {
{{ $object.Name|go }} struct {
{{ range $field := $object.Fields -}}
{{ if not $field.IsReserved -}}
{{ $field.GoFieldName }} {{ $field.ComplexitySignature }}
Expand Down Expand Up @@ -87,7 +87,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
{{ range $field := $object.Fields }}
{{ if not $field.IsReserved }}
case "{{$object.Name}}.{{$field.GoFieldName}}":
if e.complexity.{{$object.Name|toCamel}}.{{$field.GoFieldName}} == nil {
if e.complexity.{{$object.Name|go}}.{{$field.GoFieldName}} == nil {
break
}
{{ if $field.Args }}
Expand All @@ -96,7 +96,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
{{ end }}
return e.complexity.{{$object.Name|toCamel}}.{{$field.GoFieldName}}(childComplexity{{if $field.Args}}, {{$field.ComplexityArgs}} {{end}}), true
return e.complexity.{{$object.Name|go}}.{{$field.GoFieldName}}(childComplexity{{if $field.Args}}, {{$field.ComplexityArgs}} {{end}}), true
{{ end }}
{{ end }}
{{ end }}
Expand Down
188 changes: 92 additions & 96 deletions codegen/templates/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ func Funcs() template.FuncMap {
"lcFirst": lcFirst,
"quote": strconv.Quote,
"rawQuote": rawQuote,
"toCamel": ToCamel,
"dump": Dump,
"ref": ref,
"ts": TypeIdentifier,
Expand Down Expand Up @@ -228,41 +227,106 @@ func Call(p *types.Func) string {
return pkg + p.Name()
}

func ToCamel(s string) string {
if s == "_" {
return "_"
}
buffer := make([]rune, 0, len(s))
upper := true
lastWasUpper := false

for _, c := range s {
if isDelimiter(c) {
upper = true
continue
}
if !lastWasUpper && unicode.IsUpper(c) {
upper = true
func ToGo(name string) string {
runes := make([]rune, 0, len(name))

wordWalker(name, func(info *wordInfo) {
word := info.Word
if info.MatchCommonInitial {
word = strings.ToUpper(word)
} else if !info.HasCommonInitial {
word = ucFirst(strings.ToLower(word))
}
runes = append(runes, []rune(word)...)
})

if upper {
buffer = append(buffer, unicode.ToUpper(c))
} else {
buffer = append(buffer, unicode.ToLower(c))
return string(runes)
}

func ToGoPrivate(name string) string {
runes := make([]rune, 0, len(name))

first := true
wordWalker(name, func(info *wordInfo) {
word := info.Word
if first {
word = strings.ToLower(info.Word)
first = false
} else if info.MatchCommonInitial {
word = strings.ToUpper(word)
} else if !info.HasCommonInitial {
word = ucFirst(strings.ToLower(word))
}
upper = false
lastWasUpper = unicode.IsUpper(c)
}
runes = append(runes, []rune(word)...)
})

return string(buffer)
return sanitizeKeywords(string(runes))
}

func ToGo(name string) string {
return lintName(ToCamel(name))
type wordInfo struct {
Word string
MatchCommonInitial bool
HasCommonInitial bool
}

func ToGoPrivate(name string) string {
return lintName(sanitizeKeywords(lcFirst(ToCamel(name))))
// This function is based on the following code.
// https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679
func wordWalker(str string, f func(*wordInfo)) {
runes := []rune(str)
w, i := 0, 0 // index of start of word, scan
hasCommonInitial := false
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if isDelimiter(runes[i+1]) {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && isDelimiter(runes[i+n+1]) {
n++
}

// Leave at most one underscore if the underscore is between two digits
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
n--
}

copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
// lower->non-lower
eow = true
}
i++

// [w,i) is a word.
word := string(runes[w:i])
if !eow && commonInitialisms[word] && !unicode.IsLower(runes[i]) {
// through
// split IDFoo → ID, Foo
// but URLs → URLs
} else if !eow {
if commonInitialisms[word] {
hasCommonInitial = true
}
continue
}

matchCommonInitial := false
if commonInitialisms[strings.ToUpper(word)] {
hasCommonInitial = true
matchCommonInitial = true
}

f(&wordInfo{
Word: word,
MatchCommonInitial: matchCommonInitial,
HasCommonInitial: hasCommonInitial,
})
hasCommonInitial = false
w = i
}
}

var keywords = []string{
Expand Down Expand Up @@ -304,74 +368,6 @@ func sanitizeKeywords(name string) string {
return name
}

// copy from https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679
func lintName(name string) string {
// Fast path for simple cases: "_" and all lowercase.
if name == "_" {
return name
}
allLower := true
for _, r := range name {
if !unicode.IsLower(r) {
allLower = false
break
}
}
if allLower {
return name
}

// Split camelCase at any lower->upper transition, and split on underscores.
// Check each word for common initialisms.
runes := []rune(name)
w, i := 0, 0 // index of start of word, scan
for i+1 <= len(runes) {
eow := false // whether we hit the end of a word
if i+1 == len(runes) {
eow = true
} else if runes[i+1] == '_' {
// underscore; shift the remainder forward over any run of underscores
eow = true
n := 1
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
n++
}

// Leave at most one underscore if the underscore is between two digits
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
n--
}

copy(runes[i+1:], runes[i+n+1:])
runes = runes[:len(runes)-n]
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
// lower->non-lower
eow = true
}
i++
if !eow {
continue
}

// [w,i) is a word.
word := string(runes[w:i])
if u := strings.ToUpper(word); commonInitialisms[u] {
// Keep consistent case, which is lowercase only at the start.
if w == 0 && unicode.IsLower(runes[w]) {
u = strings.ToLower(u)
}
// All the common initialisms are ASCII,
// so we can replace the bytes exactly.
copy(runes[w:], []rune(u))
} else if w > 0 && strings.ToLower(word) == word {
// already all lowercase, and not the first word, so uppercase the first character.
runes[w] = unicode.ToUpper(runes[w])
}
w = i
}
return string(runes)
}

// commonInitialisms is a set of common initialisms.
// Only add entries that are highly unlikely to be non-initialisms.
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
Expand Down
87 changes: 81 additions & 6 deletions codegen/templates/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,87 @@ import (
"github.com/stretchr/testify/require"
)

func TestToUpper(t *testing.T) {
require.Equal(t, "ToCamel", ToCamel("TO_CAMEL"))
require.Equal(t, "ToCamel", ToCamel("to_camel"))
require.Equal(t, "ToCamel", ToCamel("toCamel"))
require.Equal(t, "ToCamel", ToCamel("ToCamel"))
require.Equal(t, "ToCamel", ToCamel("to-camel"))
func TestToGo(t *testing.T) {
require.Equal(t, "ToCamel", ToGo("TO_CAMEL"))
require.Equal(t, "ToCamel", ToGo("to_camel"))
require.Equal(t, "ToCamel", ToGo("toCamel"))
require.Equal(t, "ToCamel", ToGo("ToCamel"))
require.Equal(t, "ToCamel", ToGo("to-camel"))

require.Equal(t, "RelatedURLs", ToGo("RelatedURLs"))
require.Equal(t, "ImageIDs", ToGo("ImageIDs"))
require.Equal(t, "FooID", ToGo("FooID"))
require.Equal(t, "IDFoo", ToGo("IDFoo"))
require.Equal(t, "FooASCII", ToGo("FooASCII"))
require.Equal(t, "ASCIIFoo", ToGo("ASCIIFoo"))
require.Equal(t, "FooUTF8", ToGo("FooUTF8"))
require.Equal(t, "UTF8Foo", ToGo("UTF8Foo"))
require.Equal(t, "JSONEncoding", ToGo("JSONEncoding"))

require.Equal(t, "A", ToGo("A"))
require.Equal(t, "ID", ToGo("ID"))
require.Equal(t, "ID", ToGo("id"))
require.Equal(t, "", ToGo(""))

require.Equal(t, "RelatedUrls", ToGo("RelatedUrls"))
}

func TestToGoPrivate(t *testing.T) {
require.Equal(t, "toCamel", ToGoPrivate("TO_CAMEL"))
require.Equal(t, "toCamel", ToGoPrivate("to_camel"))
require.Equal(t, "toCamel", ToGoPrivate("toCamel"))
require.Equal(t, "toCamel", ToGoPrivate("ToCamel"))
require.Equal(t, "toCamel", ToGoPrivate("to-camel"))

require.Equal(t, "relatedURLs", ToGoPrivate("RelatedURLs"))
require.Equal(t, "imageIDs", ToGoPrivate("ImageIDs"))
require.Equal(t, "fooID", ToGoPrivate("FooID"))
require.Equal(t, "idFoo", ToGoPrivate("IDFoo"))
require.Equal(t, "fooASCII", ToGoPrivate("FooASCII"))
require.Equal(t, "asciiFoo", ToGoPrivate("ASCIIFoo"))
require.Equal(t, "fooUTF8", ToGoPrivate("FooUTF8"))
require.Equal(t, "utf8Foo", ToGoPrivate("UTF8Foo"))
require.Equal(t, "jsonEncoding", ToGoPrivate("JSONEncoding"))

require.Equal(t, "rangeArg", ToGoPrivate("Range"))

require.Equal(t, "a", ToGoPrivate("A"))
require.Equal(t, "id", ToGoPrivate("ID"))
require.Equal(t, "id", ToGoPrivate("id"))
require.Equal(t, "", ToGoPrivate(""))
}

func Test_wordWalker(t *testing.T) {

helper := func(str string) []*wordInfo {
resultList := []*wordInfo{}
wordWalker(str, func(info *wordInfo) {
resultList = append(resultList, info)
})
return resultList
}

require.Equal(t, []*wordInfo{{Word: "TO"}, {Word: "CAMEL"}}, helper("TO_CAMEL"))
require.Equal(t, []*wordInfo{{Word: "to"}, {Word: "camel"}}, helper("to_camel"))
require.Equal(t, []*wordInfo{{Word: "to"}, {Word: "Camel"}}, helper("toCamel"))
require.Equal(t, []*wordInfo{{Word: "To"}, {Word: "Camel"}}, helper("ToCamel"))
require.Equal(t, []*wordInfo{{Word: "to"}, {Word: "camel"}}, helper("to-camel"))

require.Equal(t, []*wordInfo{{Word: "Related"}, {Word: "URLs", HasCommonInitial: true}}, helper("RelatedURLs"))
require.Equal(t, []*wordInfo{{Word: "Image"}, {Word: "IDs", HasCommonInitial: true}}, helper("ImageIDs"))
require.Equal(t, []*wordInfo{{Word: "Foo"}, {Word: "ID", HasCommonInitial: true, MatchCommonInitial: true}}, helper("FooID"))
require.Equal(t, []*wordInfo{{Word: "ID", HasCommonInitial: true, MatchCommonInitial: true}, {Word: "Foo"}}, helper("IDFoo"))
require.Equal(t, []*wordInfo{{Word: "Foo"}, {Word: "ASCII", HasCommonInitial: true, MatchCommonInitial: true}}, helper("FooASCII"))
require.Equal(t, []*wordInfo{{Word: "ASCII", HasCommonInitial: true, MatchCommonInitial: true}, {Word: "Foo"}}, helper("ASCIIFoo"))
require.Equal(t, []*wordInfo{{Word: "Foo"}, {Word: "UTF8", HasCommonInitial: true, MatchCommonInitial: true}}, helper("FooUTF8"))
require.Equal(t, []*wordInfo{{Word: "UTF8", HasCommonInitial: true, MatchCommonInitial: true}, {Word: "Foo"}}, helper("UTF8Foo"))

require.Equal(t, []*wordInfo{{Word: "A"}}, helper("A"))
require.Equal(t, []*wordInfo{{Word: "ID", HasCommonInitial: true, MatchCommonInitial: true}}, helper("ID"))
require.Equal(t, []*wordInfo{{Word: "id", HasCommonInitial: true, MatchCommonInitial: true}}, helper("id"))
require.Equal(t, []*wordInfo{}, helper(""))

require.Equal(t, []*wordInfo{{Word: "Related"}, {Word: "Urls"}}, helper("RelatedUrls"))
}

func TestCenter(t *testing.T) {
Expand Down

0 comments on commit 5444565

Please sign in to comment.