Skip to content

Commit

Permalink
feat(generator): support self-referential fields (#802)
Browse files Browse the repository at this point in the history
Message fields may refer to the same message, directly or indirectly.
With this change the fields gain a `Recursive` attribute, set to `true`
if the field directly (or indirectly) references the containing message.

The Rust Codec uses this attribute to emit `Option<Box<T>>` instead of
`Option<T>` for such fields.

In the process I found some mistakes in the handling of map types with objects
in them.
  • Loading branch information
coryan authored Jan 24, 2025
1 parent 8aee77e commit 50a37e1
Show file tree
Hide file tree
Showing 15 changed files with 589 additions and 111 deletions.
4 changes: 4 additions & 0 deletions generator/internal/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ type Field struct {
// some helper fields. These need to be marked so they can be excluded
// from serialized messages and in other places.
Synthetic bool
// Some fields have a type that refers (sometimes indirectly) to the
// containing message. That triggers slightly different code generation for
// some languages.
Recursive bool
// A placeholder to put language specific annotations.
Codec any
}
Expand Down
50 changes: 50 additions & 0 deletions generator/internal/api/recursive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2025 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 api

func LabelRecursiveFields(model *API) {
for _, message := range model.State.MessageByID {
for _, field := range message.Fields {
visited := map[string]bool{message.ID: true}
field.Recursive = field.recursivelyReferences(message.ID, model, visited)
}
}
}

func (field *Field) recursivelyReferences(messageID string, model *API, visited map[string]bool) bool {
if field.Typez != MESSAGE_TYPE {
return false
}
if field.TypezID == messageID {
return true
}
if _, ok := visited[field.TypezID]; ok {
return false
}
if fieldMessage, ok := model.State.MessageByID[field.TypezID]; ok {
return fieldMessage.recursivelyReferences(messageID, model, visited)
}
return false
}

func (message *Message) recursivelyReferences(messageID string, model *API, visited map[string]bool) bool {
visited[message.ID] = true
for _, field := range message.Fields {
if field.recursivelyReferences(messageID, model, visited) {
return true
}
}
return false
}
256 changes: 256 additions & 0 deletions generator/internal/api/recursive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright 2025 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 api

import (
"testing"
)

func TestSimple(t *testing.T) {
field0 := &Field{
Name: "a",
Typez: STRING_TYPE,
}
field1 := &Field{
Name: "b",
Typez: MESSAGE_TYPE,
TypezID: ".test.Message",
Optional: true,
}
messages := []*Message{
{
Name: "Message",
ID: ".test.Message",
Fields: []*Field{
field0, field1,
},
},
}
model := NewTestAPI(messages, []*Enum{}, []*Service{})
LabelRecursiveFields(model)
if field0.Recursive {
t.Errorf("mismatched IsRecursive field for %v", field0)
}
if !field1.Recursive {
t.Errorf("mismatched IsRecursive field for %v", field1)
}
}

func TestSimpleMap(t *testing.T) {
field0 := &Field{
Repeated: false,
Optional: false,
Name: "children",
ID: ".test.ParentMessage.children",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage.SingularMapEntry",
}
parent := &Message{
Name: "ParentMessage",
ID: ".test.ParentMessage",
Fields: []*Field{field0},
}

key := &Field{
Name: "key",
JSONName: "key",
ID: ".test.ParentMessage.SingularMapEntry.key",
Typez: STRING_TYPE,
}
value := &Field{
Name: "value",
JSONName: "value",
ID: ".test.ParentMessage.SingularMapEntry.value",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage",
}
map_message := &Message{
Name: "SingularMapEntry",
Package: "test",
ID: ".test.ParentMessage.SingularMapEntry",
IsMap: true,
Fields: []*Field{key, value},
}

model := NewTestAPI([]*Message{parent, map_message}, []*Enum{}, []*Service{})
LabelRecursiveFields(model)
for _, field := range []*Field{value, field0} {
if !field.Recursive {
t.Errorf("expected IsRecursive to be true for field %s", field.ID)
}
}
if key.Recursive {
t.Errorf("expected IsRecursive to be false for field %s", key.ID)
}
}

func TestIndirect(t *testing.T) {
field0 := &Field{
Name: "child",
Typez: MESSAGE_TYPE,
TypezID: ".test.ChildMessage",
Optional: true,
}
field1 := &Field{
Name: "grand_child",
Typez: MESSAGE_TYPE,
TypezID: ".test.GrandChildMessage",
Optional: true,
}
field2 := &Field{
Name: "back_to_grand_parent",
Typez: MESSAGE_TYPE,
TypezID: ".test.Message",
Optional: true,
}
messages := []*Message{
{
Name: "Message",
ID: ".test.Message",
Fields: []*Field{field0},
},
{
Name: "ChildMessage",
ID: ".test.ChildMessage",
Fields: []*Field{field1},
},
{
Name: "GrandChildMessage",
ID: ".test.GrandChildMessage",
Fields: []*Field{field2},
},
}
model := NewTestAPI(messages, []*Enum{}, []*Service{})
LabelRecursiveFields(model)
for _, field := range []*Field{field0, field1, field2} {
if !field.Recursive {
t.Errorf("IsRecursive should be true for field %s", field.Name)
}
}
}

func TestViaMap(t *testing.T) {
field0 := &Field{
Name: "parent",
ID: ".test.ChildMessage.parent",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage",
}
child := &Message{
Name: "ChildMessage",
ID: ".test.ChildMessage",
Fields: []*Field{field0},
}

field1 := &Field{
Repeated: false,
Optional: false,
Name: "children",
ID: ".test.ParentMessage.children",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage.SingularMapEntry",
}
parent := &Message{
Name: "ParentMessage",
ID: ".test.ParentMessage",
Fields: []*Field{field1},
}

key := &Field{
Repeated: false,
Optional: false,
Name: "key",
JSONName: "key",
ID: ".test.ParentMessage.SingularMapEntry.key",
Typez: STRING_TYPE,
}
value := &Field{
Repeated: false,
Optional: false,
Name: "value",
JSONName: "value",
ID: ".test.ParentMessage.SingularMapEntry.value",
Typez: MESSAGE_TYPE,
TypezID: ".test.ChildMessage",
}
map_message := &Message{
Name: "SingularMapEntry",
Package: "test",
ID: ".test.ParentMessage.SingularMapEntry",
IsMap: true,
Fields: []*Field{key, value},
}

model := NewTestAPI([]*Message{parent, child, map_message}, []*Enum{}, []*Service{})
LabelRecursiveFields(model)
for _, field := range []*Field{value, field0, field1} {
if !field.Recursive {
t.Errorf("expected IsRecursive to be true for field %s", field.ID)
}
}
if key.Recursive {
t.Errorf("expected IsRecursive to be false for field %s", key.ID)
}
}

func TestReferencedCycle(t *testing.T) {
field0 := &Field{
Name: "parent",
ID: ".test.ChildMessage.parent",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage",
}
child := &Message{
Name: "ChildMessage",
ID: ".test.ChildMessage",
Fields: []*Field{field0},
}
field1 := &Field{
Name: "child",
ID: ".test.ParentMessage.child",
Typez: MESSAGE_TYPE,
TypezID: ".test.ChildMessage",
}
parent := &Message{
Name: "ParentdMessage",
ID: ".test.ParentMessage",
Fields: []*Field{field1},
}

field2 := &Field{
Name: "ref",
ID: ".test.Holder.ref",
Typez: MESSAGE_TYPE,
TypezID: ".test.ParentMessage",
}
holder := &Message{
Name: "Holder",
ID: ".test.Holder",
Fields: []*Field{field2},
}

model := NewTestAPI([]*Message{holder, parent, child}, []*Enum{}, []*Service{})
LabelRecursiveFields(model)
for _, field := range []*Field{field0, field1} {
if !field.Recursive {
t.Errorf("expected IsRecursive to be true for field %s", field.ID)
}
}
for _, field := range []*Field{field2} {
if field.Recursive {
t.Errorf("expected IsRecursive to be false for field %s", field.ID)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package language
package api

import (
"strings"
import "strings"

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

func newTestAPI(messages []*api.Message, enums []*api.Enum, services []*api.Service) *api.API {
state := &api.APIState{
MessageByID: make(map[string]*api.Message),
MethodByID: make(map[string]*api.Method),
EnumByID: make(map[string]*api.Enum),
ServiceByID: make(map[string]*api.Service),
func NewTestAPI(messages []*Message, enums []*Enum, services []*Service) *API {
state := &APIState{
MessageByID: make(map[string]*Message),
MethodByID: make(map[string]*Method),
EnumByID: make(map[string]*Enum),
ServiceByID: make(map[string]*Service),
}
for _, m := range messages {
state.MessageByID[m.ID] = m
Expand Down Expand Up @@ -58,7 +54,7 @@ func newTestAPI(messages []*api.Message, enums []*api.Enum, services []*api.Serv
}
}

return &api.API{
return &API{
Name: "Test",
Messages: messages,
Enums: enums,
Expand Down
4 changes: 2 additions & 2 deletions generator/internal/language/codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestQueryParams(t *testing.T) {
},
},
}
test := newTestAPI(
test := api.NewTestAPI(
[]*api.Message{options, request},
[]*api.Enum{},
[]*api.Service{
Expand All @@ -84,7 +84,7 @@ func TestQueryParams(t *testing.T) {
}

func TestPathParams(t *testing.T) {
test := newTestAPI(
test := api.NewTestAPI(
[]*api.Message{sample.Secret(), sample.UpdateRequest(), sample.CreateRequest()},
[]*api.Enum{},
[]*api.Service{sample.Service()},
Expand Down
Loading

0 comments on commit 50a37e1

Please sign in to comment.