diff --git a/codegen/generated!.gotpl b/codegen/generated!.gotpl index ca9e754929e..7c8afe436ea 100644 --- a/codegen/generated!.gotpl +++ b/codegen/generated!.gotpl @@ -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 }} @@ -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 }} @@ -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 }} diff --git a/codegen/templates/templates.go b/codegen/templates/templates.go index 8522824e5c9..5f0fda9d646 100644 --- a/codegen/templates/templates.go +++ b/codegen/templates/templates.go @@ -129,7 +129,6 @@ func Funcs() template.FuncMap { "lcFirst": lcFirst, "quote": strconv.Quote, "rawQuote": rawQuote, - "toCamel": ToCamel, "dump": Dump, "ref": ref, "ts": TypeIdentifier, @@ -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{ @@ -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. diff --git a/codegen/templates/templates_test.go b/codegen/templates/templates_test.go index 31f518b4bcc..cea36a3018f 100644 --- a/codegen/templates/templates_test.go +++ b/codegen/templates/templates_test.go @@ -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) {