Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl(generator): handle nested messages and enums in Rust #78

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment: all of these different case top level case conversion functions make me wonder if there is a better way to handle this and/or there is not enough flexibility at the template layer. I originally chose mustache because it is simple and does not allow you to code in the template itself. As time goes on we should monitor to make sure this is still the right choice. We could switch over to Go's template engine in the future to provide much more extensibility in the templates.

Nothing to change for now, but something we should think more about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack and agree.

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
Loading