Skip to content

Commit eb3b92e

Browse files
committed
feat(151,proto): add long-running operation support
Add support for operation support for protobuf generation. This required a minor refactor where we use a Message interface rather than concrete MessageBuilder and MessageDescriptor types directly. This allows for the abilitiy to use messages imported as resources (as is the case with aep.api.Operation).
1 parent ad62ee0 commit eb3b92e

File tree

5 files changed

+191
-48
lines changed

5 files changed

+191
-48
lines changed

pkg/api/testutils.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,54 @@ func ExampleAPI() *API {
6969
}
7070
publisher.Children = append(publisher.Children, book)
7171

72+
// Resource to test operation logic
73+
tome := &Resource{
74+
Singular: "tome",
75+
Plural: "tomes",
76+
Parents: []*Resource{publisher},
77+
Schema: &openapi.Schema{
78+
Type: "object",
79+
Properties: map[string]openapi.Schema{
80+
"name": {Type: "string", XAEPFieldNumber: 1},
81+
"id": {Type: "string", XAEPFieldNumber: 2},
82+
},
83+
},
84+
Methods: Methods{
85+
List: &ListMethod{},
86+
Get: &GetMethod{},
87+
Apply: &ApplyMethod{
88+
IsLongRunning: true,
89+
},
90+
Create: &CreateMethod{
91+
IsLongRunning: true,
92+
},
93+
Update: &UpdateMethod{
94+
IsLongRunning: true,
95+
},
96+
Delete: &DeleteMethod{
97+
IsLongRunning: true,
98+
},
99+
},
100+
CustomMethods: []*CustomMethod{
101+
{
102+
Name: "archive",
103+
Method: "POST",
104+
Request: &openapi.Schema{
105+
Type: "object",
106+
Properties: map[string]openapi.Schema{},
107+
},
108+
Response: &openapi.Schema{
109+
Type: "object",
110+
Properties: map[string]openapi.Schema{
111+
"archived": {Type: "boolean", XAEPFieldNumber: 1},
112+
},
113+
},
114+
IsLongRunning: true,
115+
},
116+
},
117+
}
118+
publisher.Children = append(publisher.Children, tome)
119+
72120
// Create book-edition resource
73121
bookEdition := &Resource{
74122
Singular: "book-edition",
@@ -109,6 +157,7 @@ func ExampleAPI() *API {
109157
"book-edition": bookEdition,
110158
"publisher": publisher,
111159
"operation": OperationResourceWithDefaults(),
160+
"tome": tome,
112161
},
113162
}
114163
}

pkg/proto/message.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package proto
2+
3+
import (
4+
"github.com/jhump/protoreflect/desc"
5+
"github.com/jhump/protoreflect/desc/builder"
6+
"google.golang.org/protobuf/types/descriptorpb"
7+
)
8+
9+
// type Message wraps a MessageBuilder or a
10+
// message descriptor. It abstracts those concrete
11+
// implementations to provide a common interface.
12+
type Message interface {
13+
FieldType() *builder.FieldType
14+
RpcType() *builder.RpcType
15+
Options() *descriptorpb.MessageOptions
16+
AddMessage(fb *builder.FileBuilder)
17+
}
18+
19+
// A variant that wraps MessageBuilder, for messages
20+
// created during generation.
21+
type WrappedMessageBuilder struct {
22+
mb *builder.MessageBuilder
23+
}
24+
25+
func NewWrappedMessageBuilder(mb *builder.MessageBuilder) WrappedMessageBuilder {
26+
return WrappedMessageBuilder{mb: mb}
27+
}
28+
29+
func (wrapped WrappedMessageBuilder) FieldType() *builder.FieldType {
30+
return builder.FieldTypeMessage(wrapped.mb)
31+
}
32+
33+
func (wrapped WrappedMessageBuilder) Options() *descriptorpb.MessageOptions {
34+
return wrapped.mb.Options
35+
}
36+
37+
func (wrapped WrappedMessageBuilder) RpcType() *builder.RpcType {
38+
return builder.RpcTypeMessage(wrapped.mb, false)
39+
}
40+
41+
func (wrapped WrappedMessageBuilder) AddMessage(fb *builder.FileBuilder) {
42+
fb.AddMessage(wrapped.mb)
43+
}
44+
45+
// A variant that wraps MessageDescriptor, for messages
46+
// referenced
47+
type WrappedMessageDescriptor struct {
48+
md *desc.MessageDescriptor
49+
}
50+
51+
func NewWrappedMessageDescriptor(md *desc.MessageDescriptor) WrappedMessageDescriptor {
52+
return WrappedMessageDescriptor{md: md}
53+
}
54+
55+
func (wrapped WrappedMessageDescriptor) FieldType() *builder.FieldType {
56+
return builder.FieldTypeImportedMessage(wrapped.md)
57+
}
58+
59+
func (wrapped WrappedMessageDescriptor) Options() *descriptorpb.MessageOptions {
60+
return nil
61+
}
62+
63+
func (wrapped WrappedMessageDescriptor) RpcType() *builder.RpcType {
64+
return builder.RpcTypeImportedMessage(wrapped.md, false)
65+
}
66+
67+
func (wrapped WrappedMessageDescriptor) AddMessage(fb *builder.FileBuilder) {
68+
// noop
69+
}

pkg/proto/proto.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
)
3131

3232
type MessageStorage struct {
33-
Messages map[string]*builder.MessageBuilder
33+
Messages map[string]Message
3434
}
3535

3636
func APIToProtoString(a *api.API, outputDir string) ([]byte, error) {
@@ -50,7 +50,7 @@ func APIToProtoString(a *api.API, outputDir string) ([]byte, error) {
5050
}
5151

5252
func APIToProto(a *api.API, outputDir string) (*desc.FileDescriptor, error) {
53-
m := &MessageStorage{Messages: map[string]*builder.MessageBuilder{}}
53+
m := &MessageStorage{Messages: map[string]Message{}}
5454
dir, file := filepath.Split(outputDir)
5555
packageParts := []string{file}
5656
for dir != "." {
@@ -130,7 +130,7 @@ func GenerateSchemaMessages(a *api.API, m *MessageStorage, fb *builder.FileBuild
130130
if err != nil {
131131
return err
132132
}
133-
fb.AddMessage(mb)
133+
mb.AddMessage(fb)
134134
}
135135
return nil
136136
}

pkg/proto/proto_test.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func TestAPIToProto(t *testing.T) {
3232
expectError bool
3333
expectMessages []string
3434
expectMethods []string
35+
expectStrings []string
3536
}{
3637
{
3738
name: "BasicAPItoProtoConversion",
@@ -58,6 +59,7 @@ func TestAPIToProto(t *testing.T) {
5859
"GetBookEditionRequest",
5960
"ListBookEditionsRequest",
6061
"ListBookEditionsResponse",
62+
"ArchiveTomeRequest",
6163
},
6264
expectMethods: []string{
6365
"CreatePublisher",
@@ -72,6 +74,10 @@ func TestAPIToProto(t *testing.T) {
7274
"GetBookEdition",
7375
"ListBookEditions",
7476
},
77+
expectStrings: []string{
78+
// verify that ArchiveTome is a long-running-operation.
79+
"rpc ArchiveTome ( ArchiveTomeRequest ) returns ( aep.api.Operation ) {",
80+
},
7581
},
7682
}
7783

@@ -108,7 +114,7 @@ func TestAPIToProto(t *testing.T) {
108114

109115
protoContent := string(protoString)
110116
// Print the proto content for debugging
111-
// t.Logf("Proto content: \n%s", protoContent)
117+
t.Logf("Proto content: \n---\n%s\n---", protoContent)
112118

113119
// Check for expected messages
114120
for _, msgName := range tt.expectMessages {
@@ -129,6 +135,13 @@ func TestAPIToProto(t *testing.T) {
129135
"Expected method %s not found in proto content", methodName)
130136
}
131137

138+
// Check for expected strings
139+
for _, expectedString := range tt.expectStrings {
140+
assert.True(t,
141+
strings.Contains(protoContent, expectedString),
142+
"Expected string %s not found in proto content", expectedString)
143+
}
144+
132145
// Verify correct parent-child relationships in the API paths
133146
assert.True(t, strings.Contains(protoContent, "get: \"/{path=publishers/*/books/*}\""))
134147
assert.True(t, strings.Contains(protoContent, "get: \"/{path=publishers/*/books/*/editions/*}\""))

0 commit comments

Comments
 (0)