Skip to content

Commit

Permalink
impl(generator): handle nested messages and enums in Rust (#78)
Browse files Browse the repository at this point in the history
The mapping nested messages and enums uses a public module within the crate.
That requires a new "snake case" accessor for the message name, as Rust uses
snake case for modules.

We also need a boolean to find out if the module is needed at all.

And we need to get a different attribute for the fully qualified name of each
message and enum, as we must refer to the qualified name of the generated
struct when using the type.
  • Loading branch information
coryan authored Nov 4, 2024
1 parent e74d91d commit 5f39c13
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 190 deletions.
66 changes: 63 additions & 3 deletions generator/internal/genclient/genclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,24 @@ type LanguageCodec interface {
// FieldType returns a string representation of a message field type.
FieldType(f *Field, state *APIState) string
MethodInOutTypeName(id string, state *APIState) string
// MessageName returns a string representation of a message name.
// The (unqualified) message name, as used when defining the type to
// represent it.
MessageName(m *Message, state *APIState) string
// EnumName returns a string representation of an enum name.
// The fully-qualified message name, as used when referring to the name from
// another place in the package.
FQMessageName(m *Message, state *APIState) string
// The (unqualified) enum name, as used when defining the type to
// represent it.
EnumName(e *Enum, state *APIState) string
// EnumValueName returns a string representation of an enum value name.
// The fully-qualified enum name, as used when referring to the name from
// another place in the package.
FQEnumName(e *Enum, state *APIState) string
// The (unqualified) enum value name, as used when defining the constant,
// variable, or enum value that holds it.
EnumValueName(e *EnumValue, state *APIState) string
// The fully qualified enum value name, as used when using the constant,
// variable, or enum value that hodls it.
FQEnumValueName(e *EnumValue, state *APIState) string
// BodyAccessor returns a string representation of the accessor used to
// get the body out of a request. For instance this might return `.Body()`.
BodyAccessor(m *Method, state *APIState) string
Expand Down Expand Up @@ -132,3 +144,51 @@ func Generate(req *GenerateRequest) (*Output, error) {
var output *Output
return output, nil
}

// Creates a populated API state from lists of messages, enums, and services.
func NewTestAPI(messages []*Message, enums []*Enum, services []*Service) *API {
state := &APIState{
MessageByID: make(map[string]*Message),
EnumByID: make(map[string]*Enum),
ServiceByID: make(map[string]*Service),
}
for _, m := range messages {
state.MessageByID[m.ID] = m
}
for _, e := range enums {
state.EnumByID[e.ID] = e
}
for _, s := range services {
state.ServiceByID[s.ID] = s
}
for _, m := range messages {
parentID := parentName(m.ID)
parent := state.MessageByID[parentID]
if parent != nil {
m.Parent = parent
parent.Messages = append(parent.Messages, m)
}
}
for _, e := range enums {
parent := state.MessageByID[parentName(e.ID)]
if parent != nil {
e.Parent = parent
parent.Enums = append(parent.Enums, e)
}
}

return &API{
Name: "Test",
Messages: messages,
Enums: enums,
Services: services,
State: state,
}
}

func parentName(id string) string {
if lastIndex := strings.LastIndex(id, "."); lastIndex != -1 {
return id[:lastIndex]
}
return "."
}
12 changes: 12 additions & 0 deletions generator/internal/genclient/language/internal/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,32 @@ func (c *Codec) MessageName(m *genclient.Message, state *genclient.APIState) str
return strcase.ToCamel(m.Name)
}

func (c *Codec) FQMessageName(m *genclient.Message, state *genclient.APIState) string {
return c.MessageName(m, state)
}

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)
}

func (c *Codec) FQEnumName(e *genclient.Enum, state *genclient.APIState) string {
return c.EnumName(e, state)
}

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

func (c *Codec) FQEnumValueName(v *genclient.EnumValue, state *genclient.APIState) string {
return c.EnumValueName(v, state)
}

func (c *Codec) BodyAccessor(m *genclient.Method, state *genclient.APIState) string {
if m.HTTPInfo.Body == "*" {
// no accessor needed, use the whole request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package golang

import (
"testing"

"github.com/googleapis/google-cloud-rust/generator/internal/genclient"
)

type CaseConvertTest struct {
Expand Down Expand Up @@ -53,3 +55,70 @@ func TestToPascal(t *testing.T) {
}
}
}

func TestMessageNames(t *testing.T) {
message := &genclient.Message{
Name: "Replication",
ID: "..Replication",
Fields: []*genclient.Field{
{
Name: "automatic",
Typez: genclient.MESSAGE_TYPE,
TypezID: "..Automatic",
Optional: true,
Repeated: false,
},
},
}
nested := &genclient.Message{
Name: "Automatic",
ID: "..Replication.Automatic",
}

api := genclient.NewTestAPI([]*genclient.Message{message, nested}, []*genclient.Enum{}, []*genclient.Service{})

c := &Codec{}
if got := c.MessageName(message, api.State); got != "Replication" {
t.Errorf("mismatched message name, want=Replication, got=%s", got)
}
if got := c.FQMessageName(message, api.State); got != "Replication" {
t.Errorf("mismatched message name, want=Replication, got=%s", got)
}

if got := c.MessageName(nested, api.State); got != "Replication_Automatic" {
t.Errorf("mismatched message name, want=SecretVersion_Automatic, got=%s", got)
}
if got := c.FQMessageName(nested, api.State); got != "Replication_Automatic" {
t.Errorf("mismatched message name, want=Replication_Automatic, got=%s", got)
}
}

func TestEnumNames(t *testing.T) {
message := &genclient.Message{
Name: "SecretVersion",
ID: "..SecretVersion",
Fields: []*genclient.Field{
{
Name: "automatic",
Typez: genclient.MESSAGE_TYPE,
TypezID: "..Automatic",
Optional: true,
Repeated: false,
},
},
}
nested := &genclient.Enum{
Name: "State",
ID: "..SecretVersion.State",
}

api := genclient.NewTestAPI([]*genclient.Message{message}, []*genclient.Enum{nested}, []*genclient.Service{})

c := &Codec{}
if got := c.EnumName(nested, api.State); got != "SecretVersion_State" {
t.Errorf("mismatched message name, want=SecretVersion_Automatic, got=%s", got)
}
if got := c.FQEnumName(nested, api.State); got != "SecretVersion_State" {
t.Errorf("mismatched message name, want=SecretVersion_State, got=%s", got)
}
}
40 changes: 31 additions & 9 deletions generator/internal/genclient/language/internal/rust/rust.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewCodec() *Codec {
type Codec struct{}

func (c *Codec) LoadWellKnownTypes(s *genclient.APIState) {
// TODO(#77) - replace these placeholders with real types
timestamp := &genclient.Message{
ID: ".google.protobuf.Timestamp",
Name: "String",
Expand Down Expand Up @@ -98,14 +99,17 @@ func (c *Codec) FieldType(f *genclient.Field, state *genclient.APIState) string
val := c.FieldType(m.Fields[1], state)
return "Option<std::collections::HashMap<" + key + "," + val + ">>"
}
return "Option<" + c.MessageName(m, state) + ">"
if strings.HasPrefix(m.ID, ".google.protobuf.") {
return "Option<String> /* TODO(#77) - handle " + f.TypezID + " */"
}
return "Option<" + c.FQMessageName(m, state) + ">"
} else if f.Typez == genclient.ENUM_TYPE {
e, ok := state.EnumByID[f.TypezID]
if !ok {
slog.Error("unable to lookup type", "id", f.TypezID)
return ""
}
return c.EnumName(e, state)
return c.FQEnumName(e, state)
} else if f.Typez == genclient.GROUP_TYPE {
slog.Error("TODO(#39) - better handling of `oneof` fields")
return ""
Expand All @@ -130,29 +134,47 @@ func (c *Codec) MethodInOutTypeName(id string, s *genclient.APIState) string {
}

func (c *Codec) MessageName(m *genclient.Message, state *genclient.APIState) string {
if m.Parent != nil {
return c.MessageName(m.Parent, state) + "_" + strcase.ToCamel(m.Name)
}
if m.Package != "" {
return m.Package + "." + strcase.ToCamel(m.Name)
return m.Package + "." + c.ToPascal(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)
func (c *Codec) messageScopeName(m *genclient.Message) string {
if m == nil {
return "crate"
}
return c.messageScopeName(m.Parent) + "::" + c.ToSnake(m.Name)
}

func (c *Codec) enumScopeName(e *genclient.Enum) string {
return c.messageScopeName(e.Parent)
}

func (c *Codec) FQMessageName(m *genclient.Message, _ *genclient.APIState) string {
return c.messageScopeName(m.Parent) + "::" + c.ToPascal(m.Name)
}

func (c *Codec) EnumName(e *genclient.Enum, state *genclient.APIState) string {
return c.ToPascal(e.Name)
}

func (c *Codec) FQEnumName(e *genclient.Enum, _ *genclient.APIState) string {
return c.messageScopeName(e.Parent) + "::" + 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 c.ToPascal(e.Name)
}

func (c *Codec) FQEnumValueName(v *genclient.EnumValue, _ *genclient.APIState) string {
// TODO(#76) - these will be `const` strings and therefore should be SNAKE_UPPERCASE.
return c.enumScopeName(v.Parent) + "::" + c.ToSnake(v.Name)
}

func (c *Codec) BodyAccessor(m *genclient.Method, state *genclient.APIState) string {
if m.HTTPInfo.Body == "*" {
// no accessor needed, use the whole request
Expand Down
67 changes: 67 additions & 0 deletions generator/internal/genclient/language/internal/rust/rust_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,70 @@ func TestToPascal(t *testing.T) {
}
}
}

func TestMessageNames(t *testing.T) {
message := &genclient.Message{
Name: "Replication",
ID: "..Replication",
Fields: []*genclient.Field{
{
Name: "automatic",
Typez: genclient.MESSAGE_TYPE,
TypezID: "..Automatic",
Optional: true,
Repeated: false,
},
},
}
nested := &genclient.Message{
Name: "Automatic",
ID: "..Replication.Automatic",
}

api := genclient.NewTestAPI([]*genclient.Message{message, nested}, []*genclient.Enum{}, []*genclient.Service{})

c := &Codec{}
if got := c.MessageName(message, api.State); got != "Replication" {
t.Errorf("mismatched message name, got=%s, want=Replication", got)
}
if got := c.FQMessageName(message, api.State); got != "crate::Replication" {
t.Errorf("mismatched message name, got=%s, want=crate::Replication", got)
}

if got := c.MessageName(nested, api.State); got != "Automatic" {
t.Errorf("mismatched message name, got=%s, want=Automatic", got)
}
if got := c.FQMessageName(nested, api.State); got != "crate::replication::Automatic" {
t.Errorf("mismatched message name, got=%s, want=crate::replication::Automatic", got)
}
}

func TestEnumNames(t *testing.T) {
message := &genclient.Message{
Name: "SecretVersion",
ID: "..SecretVersion",
Fields: []*genclient.Field{
{
Name: "automatic",
Typez: genclient.MESSAGE_TYPE,
TypezID: "..Automatic",
Optional: true,
Repeated: false,
},
},
}
nested := &genclient.Enum{
Name: "State",
ID: "..SecretVersion.State",
}

api := genclient.NewTestAPI([]*genclient.Message{message}, []*genclient.Enum{nested}, []*genclient.Service{})

c := &Codec{}
if got := c.EnumName(nested, api.State); got != "State" {
t.Errorf("mismatched message name, got=%s, want=Automatic", got)
}
if got := c.FQEnumName(nested, api.State); got != "crate::secret_version::State" {
t.Errorf("mismatched message name, got=%s, want=crate::secret_version::State", got)
}
}
21 changes: 20 additions & 1 deletion generator/internal/genclient/templatedata.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,30 @@ func (m *message) Enums() []*enum {
})
}

// NameToCamel converts a Name to CamelCase.
func (m *message) Name() string {
return m.c.MessageName(m.s, m.state)
}

func (m *message) QualifiedName() string {
return m.c.FQMessageName(m.s, m.state)
}

func (m *message) NameSnakeCase() string {
return m.c.ToSnake(m.s.Name)
}

func (m *message) HasNestedTypes() bool {
if len(m.s.Enums) > 0 {
return true
}
for _, child := range m.s.Messages {
if !child.IsMap {
return true
}
}
return false
}

func (m *message) DocLines() []string {
// TODO(codyoss): https://github.com/googleapis/google-cloud-rust/issues/33
ss := strings.Split(m.s.Documentation, "\n")
Expand Down
Loading

0 comments on commit 5f39c13

Please sign in to comment.