diff --git a/generator/cmd/protoc-gen-gclient/testdata/rust/golden/model.rs b/generator/cmd/protoc-gen-gclient/testdata/rust/golden/model.rs index b522d541f..7365445a1 100755 --- a/generator/cmd/protoc-gen-gclient/testdata/rust/golden/model.rs +++ b/generator/cmd/protoc-gen-gclient/testdata/rust/golden/model.rs @@ -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 diff --git a/generator/internal/genclient/genclient.go b/generator/internal/genclient/genclient.go index 38422fe41..ac762836e 100644 --- a/generator/internal/genclient/genclient.go +++ b/generator/internal/genclient/genclient.go @@ -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. diff --git a/generator/internal/genclient/language/internal/golang/golang.go b/generator/internal/genclient/language/internal/golang/golang.go index 178e3146f..ed3b11e2c 100644 --- a/generator/internal/genclient/language/internal/golang/golang.go +++ b/generator/internal/genclient/language/internal/golang/golang.go @@ -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 + "_" +} diff --git a/generator/internal/genclient/language/internal/golang/golang_test.go b/generator/internal/genclient/language/internal/golang/golang_test.go new file mode 100644 index 000000000..50e7186f3 --- /dev/null +++ b/generator/internal/genclient/language/internal/golang/golang_test.go @@ -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) + } + } +} diff --git a/generator/internal/genclient/language/internal/rust/rust.go b/generator/internal/genclient/language/internal/rust/rust.go index b4784a922..8d971a8c5 100644 --- a/generator/internal/genclient/language/internal/rust/rust.go +++ b/generator/internal/genclient/language/internal/rust/rust.go @@ -17,6 +17,7 @@ package rust import ( "fmt" "log/slog" + "strings" "github.com/googleapis/google-cloud-rust/generator/internal/genclient" "github.com/iancoleman/strcase" @@ -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 { @@ -135,21 +136,21 @@ 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 { @@ -157,17 +158,18 @@ func (c *Codec) BodyAccessor(m *genclient.Method, state *genclient.APIState) str // 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 } @@ -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 +} diff --git a/generator/internal/genclient/language/internal/rust/rust_test.go b/generator/internal/genclient/language/internal/rust/rust_test.go index ad46a132c..7aea6637c 100644 --- a/generator/internal/genclient/language/internal/rust/rust_test.go +++ b/generator/internal/genclient/language/internal/rust/rust_test.go @@ -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) + } + } +} diff --git a/generator/internal/genclient/templatedata.go b/generator/internal/genclient/templatedata.go index d40bd8f64..d5df31aee 100644 --- a/generator/internal/genclient/templatedata.go +++ b/generator/internal/genclient/templatedata.go @@ -82,22 +82,22 @@ func (s *service) Methods() []*method { // NameToSnake converts Name to snake_case. func (s *service) NameToSnake() string { - return strcase.ToSnake(s.s.Name) + return s.c.ToSnake(s.s.Name) } -// NameToCamel converts a Name to CamelCase. -func (s *service) NameToCamel() string { - return s.ServiceNameToCamel() +// NameToPascanl converts a Name to PascalCase. +func (s *service) NameToPascal() string { + return s.ServiceNameToPascal() } -// NameToCamel converts a Name to CamelCase. -func (s *service) ServiceNameToCamel() string { - return strcase.ToCamel(s.s.Name) +// NameToPascal converts a Name to PascalCase. +func (s *service) ServiceNameToPascal() string { + return s.c.ToPascal(s.s.Name) } -// NameToLowerCamel coverts Name to camelCase -func (s *service) NameToLowerCamel() string { - return strcase.ToLowerCamel(s.s.Name) +// NameToCamel coverts Name to camelCase +func (s *service) NameToCamel() string { + return s.c.ToCamel(s.s.Name) } func (s *service) DocLines() []string { @@ -120,7 +120,7 @@ func (m *method) NameToSnake() string { return strcase.ToSnake(m.s.Name) } -// NameToCamel converts a Name to CamelCase. +// NameToCamel converts a Name to camelCase. func (m *method) NameToCamel() string { return strcase.ToCamel(m.s.Name) } @@ -285,12 +285,12 @@ type field struct { // NameToSnake converts a Name to snake_case. func (f *field) NameToSnake() string { - return strcase.ToSnake(f.s.Name) + return f.c.ToSnake(f.s.Name) } // NameToCamel converts a Name to camelCase. func (f *field) NameToCamel() string { - return strcase.ToCamel(f.s.Name) + return f.c.ToCamel(f.s.Name) } func (f *field) DocLines() []string { diff --git a/generator/templates/rust/lib.rs.mustache b/generator/templates/rust/lib.rs.mustache index 322b00477..085da9653 100644 --- a/generator/templates/rust/lib.rs.mustache +++ b/generator/templates/rust/lib.rs.mustache @@ -29,8 +29,8 @@ impl Client { {{#DocLines}} /// {{{.}}} {{/DocLines}} - pub fn {{NameToSnake}}(&self) -> {{NameToCamel}} { - {{NameToCamel}} { + pub fn {{NameToSnake}}(&self) -> {{NameToPascal}} { + {{NameToPascal}} { client: self.clone(), base_path: "https://{{DefaultHost}}/".to_string(), } @@ -43,12 +43,12 @@ impl Client { /// {{{.}}} {{/DocLines}} #[derive(Debug)] -pub struct {{NameToCamel}} { +pub struct {{NameToPascal}} { client: Client, base_path: String, } -impl {{NameToCamel}} { +impl {{NameToPascal}} { {{#Methods}} {{#DocLines}} @@ -80,4 +80,4 @@ impl {{NameToCamel}} { } {{/Methods}} } -{{/Services}} \ No newline at end of file +{{/Services}}