Skip to content

Commit

Permalink
impl: better name conversions (#69)
Browse files Browse the repository at this point in the history
This PR always uses the per-language Codec to perform any "case conversion". We
need to involve the Codec because each language has a different number of
reserved words that must be escaped, and the escaping is also language
specific.

I also fixed a number of places where we used "camel case" when we meant
`PascalCase` (note the starting upper case, as opposed to `camelCase`).

Almost incidentally, I fixed the conversion for `data_crc32c` (and other names
already in `snake_case` form). Previously that got converted to
`data_crc_32_c`.
  • Loading branch information
coryan authored Nov 1, 2024
1 parent 314cf98 commit 2920296
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ pub struct SecretPayload {
/// The CRC32C value is encoded as a Int64 for compatibility, and can be
/// safely downconverted to uint32 in languages that support this type.
/// https://cloud.google.com/apis/design/design_patterns#integer_types
pub data_crc_32_c: i64,
pub data_crc32c: i64,
}

/// Request message for
Expand Down
10 changes: 10 additions & 0 deletions generator/internal/genclient/genclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ type LanguageCodec interface {
// An example return value might be
// `&Pair{Key: "secretId", Value: "req.SecretId()"}`
QueryParams(m *Method, state *APIState) []*Pair
// ToSnake converts a symbol name to `snake_case`, applying any mangling
// required by the language, e.g., to avoid clashes with reserved words.
ToSnake(string) string
// ToPascal converts a symbol name to `PascalCase`, applying any mangling
// required by the language, e.g., to avoid clashes with reserved words.
ToPascal(string) string
// ToCamel converts a symbol name to `camelCase` (sometimes called
// "lowercase CamelCase"), applying any mangling required by the language,
// e.g., to avoid clashes with reserved words.
ToCamel(string) string
}

// GenerateRequest used to generate clients.
Expand Down
53 changes: 53 additions & 0 deletions generator/internal/genclient/language/internal/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,56 @@ func (c *Codec) QueryParams(m *genclient.Method, state *genclient.APIState) []*g
}
return queryParams
}

func (*Codec) ToSnake(symbol string) string {
if strings.ToLower(symbol) == symbol {
return EscapeKeyword(symbol)
}
return EscapeKeyword(strcase.ToSnake(symbol))
}

func (*Codec) ToPascal(symbol string) string {
return EscapeKeyword(strcase.ToCamel(symbol))
}

func (*Codec) ToCamel(symbol string) string {
return strcase.ToLowerCamel(symbol)
}

// The list of Golang keywords and reserved words can be found at:
//
// https://go.dev/ref/spec#Keywords
func EscapeKeyword(symbol string) string {
keywords := map[string]bool{
"break": true,
"default": true,
"func": true,
"interface": true,
"select": true,
"case": true,
"defer": true,
"go": true,
"map": true,
"struct": true,
"chan": true,
"else": true,
"goto": true,
"package": true,
"switch": true,
"const": true,
"fallthrough": true,
"if": true,
"range": true,
"type": true,
"continue": true,
"for": true,
"import": true,
"return": true,
"var": true,
}
_, ok := keywords[symbol]
if !ok {
return symbol
}
return symbol + "_"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package golang

import (
"testing"
)

type CaseConvertTest struct {
Input string
Expected string
}

func TestToSnake(t *testing.T) {
c := &Codec{}
var snakeConvertTests = []CaseConvertTest{
{"FooBar", "foo_bar"},
{"foo_bar", "foo_bar"},
{"data_crc32c", "data_crc32c"},
{"Map", "map_"},
{"switch", "switch_"},
}
for _, test := range snakeConvertTests {
if output := c.ToSnake(test.Input); output != test.Expected {
t.Errorf("Output %s not equal to expected %s, input=%s", output, test.Expected, test.Input)
}
}
}

func TestToPascal(t *testing.T) {
c := &Codec{}
var pascalConvertTests = []CaseConvertTest{
{"foo_bar", "FooBar"},
{"FooBar", "FooBar"},
{"True", "True"},
{"return", "Return"},
}
for _, test := range pascalConvertTests {
if output := c.ToPascal(test.Input); output != test.Expected {
t.Errorf("Output %s not equal to expected %s, input=%s", output, test.Expected, test.Input)
}
}
}
114 changes: 107 additions & 7 deletions generator/internal/genclient/language/internal/rust/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package rust
import (
"fmt"
"log/slog"
"strings"

"github.com/googleapis/google-cloud-rust/generator/internal/genclient"
"github.com/iancoleman/strcase"
Expand Down Expand Up @@ -125,7 +126,7 @@ func (c *Codec) MethodInOutTypeName(id string, s *genclient.APIState) string {
slog.Error("unable to lookup type", "id", id)
return ""
}
return strcase.ToCamel(m.Name)
return c.ToPascal(m.Name)
}

func (c *Codec) MessageName(m *genclient.Message, state *genclient.APIState) string {
Expand All @@ -135,39 +136,40 @@ func (c *Codec) MessageName(m *genclient.Message, state *genclient.APIState) str
if m.Package != "" {
return m.Package + "." + strcase.ToCamel(m.Name)
}
return strcase.ToCamel(m.Name)
return c.ToPascal(m.Name)
}

func (c *Codec) EnumName(e *genclient.Enum, state *genclient.APIState) string {
if e.Parent != nil {
return c.MessageName(e.Parent, state) + "_" + strcase.ToCamel(e.Name)
}
return strcase.ToCamel(e.Name)
return c.ToPascal(e.Name)
}

func (c *Codec) EnumValueName(e *genclient.EnumValue, state *genclient.APIState) string {
if e.Parent.Parent != nil {
return c.MessageName(e.Parent.Parent, state) + "_" + strcase.ToCamel(e.Name)
}
return strcase.ToCamel(e.Name)
return c.ToPascal(e.Name)
}

func (c *Codec) BodyAccessor(m *genclient.Method, state *genclient.APIState) string {
if m.HTTPInfo.Body == "*" {
// no accessor needed, use the whole request
return ""
}
return "." + strcase.ToSnake(m.HTTPInfo.Body)
return "." + c.ToSnake(m.HTTPInfo.Body)
}

func (c *Codec) HTTPPathFmt(m *genclient.HTTPInfo, state *genclient.APIState) string {
return genclient.HTTPPathVarRegex.ReplaceAllStringFunc(m.RawPath, func(s string) string { return "{}" })
}

func (c *Codec) HTTPPathArgs(h *genclient.HTTPInfo, state *genclient.APIState) []string {
var args []string
rawArgs := h.PathArgs()
for _, arg := range rawArgs {
args = append(args, "req."+strcase.ToSnake(arg))
args = append(args, "req."+c.ToSnake(arg))
}
return args
}
Expand All @@ -183,8 +185,106 @@ func (c *Codec) QueryParams(m *genclient.Method, state *genclient.APIState) []*g
var queryParams []*genclient.Pair
for _, field := range msg.Fields {
if field.JSONName != "" && !notQuery[field.JSONName] {
queryParams = append(queryParams, &genclient.Pair{Key: field.JSONName, Value: "req." + strcase.ToSnake(field.JSONName) + ".as_str()"})
queryParams = append(queryParams, &genclient.Pair{Key: field.JSONName, Value: "req." + c.ToSnake(field.JSONName) + ".as_str()"})
}
}
return queryParams
}

// Convert a name to `snake_case`. The Rust naming conventions use this style
// for modules, fields, and functions.
//
// This type of conversion can easily introduce keywords. Consider
//
// `ToSnake("True") -> "true"`
func (*Codec) ToSnake(symbol string) string {
if strings.ToLower(symbol) == symbol {
return EscapeKeyword(symbol)
}
return EscapeKeyword(strcase.ToSnake(symbol))
}

// Convert a name to `PascalCase`. Strangley, the `strcase` package calls this
// `ToCamel` while usually `camelCase` starts with a lowercase letter. The
// Rust naming convensions use this style for structs, enums and traits.
//
// This type of conversion rarely introduces keywords. The one example is
//
// `ToPascal("self") -> "Self"`
func (*Codec) ToPascal(symbol string) string {
return EscapeKeyword(strcase.ToCamel(symbol))
}

func (*Codec) ToCamel(symbol string) string {
return EscapeKeyword(strcase.ToLowerCamel(symbol))
}

// The list of Rust keywords and reserved words can be found at:
//
// https://doc.rust-lang.org/reference/keywords.html
func EscapeKeyword(symbol string) string {
keywords := map[string]bool{
"as": true,
"break": true,
"const": true,
"continue": true,
"crate": true,
"else": true,
"enum": true,
"extern": true,
"false": true,
"fn": true,
"for": true,
"if": true,
"impl": true,
"in": true,
"let": true,
"loop": true,
"match": true,
"mod": true,
"move": true,
"mut": true,
"pub": true,
"ref": true,
"return": true,
"self": true,
"Self": true,
"static": true,
"struct": true,
"super": true,
"trait": true,
"true": true,
"type": true,
"unsafe": true,
"use": true,
"where": true,
"while": true,

// Keywords in Rust 2018+.
"async": true,
"await": true,
"dyn": true,

// Reserved
"abstract": true,
"become": true,
"box": true,
"do": true,
"final": true,
"macro": true,
"override": true,
"priv": true,
"typeof": true,
"unsized": true,
"virtual": true,
"yield": true,

// Reserved in Rust 2018+
"try": true,
}
_, ok := keywords[symbol]
if !ok {
return symbol
}
return "r#" + symbol
}
42 changes: 42 additions & 0 deletions generator/internal/genclient/language/internal/rust/rust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,45 @@ func TestScalarFields(t *testing.T) {
}
}
}

type CaseConvertTest struct {
Input string
Expected string
}

func TestToSnake(t *testing.T) {
c := &Codec{}
var snakeConvertTests = []CaseConvertTest{
{"FooBar", "foo_bar"},
{"foo_bar", "foo_bar"},
{"data_crc32c", "data_crc32c"},
{"True", "r#true"},
{"Static", "r#static"},
{"Trait", "r#trait"},
{"Self", "r#self"},
{"self", "r#self"},
{"yield", "r#yield"},
}
for _, test := range snakeConvertTests {
if output := c.ToSnake(test.Input); output != test.Expected {
t.Errorf("Output %q not equal to expected %q, input=%s", output, test.Expected, test.Input)
}
}
}

func TestToPascal(t *testing.T) {
c := &Codec{}
var pascalConvertTests = []CaseConvertTest{
{"foo_bar", "FooBar"},
{"FooBar", "FooBar"},
{"True", "True"},
{"Self", "r#Self"},
{"self", "r#Self"},
{"yield", "Yield"},
}
for _, test := range pascalConvertTests {
if output := c.ToPascal(test.Input); output != test.Expected {
t.Errorf("Output %q not equal to expected %q", output, test.Expected)
}
}
}
Loading

0 comments on commit 2920296

Please sign in to comment.