Skip to content

Commit 99f2273

Browse files
authored
feat(gengapic): implement diregapic lro foundation + polling (#866)
1 parent 0da60de commit 99f2273

17 files changed

+544
-76
lines changed

WORKSPACE

+19-7
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ http_archive(
2222
],
2323
)
2424

25-
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
26-
27-
go_rules_dependencies()
28-
29-
go_register_toolchains(version = "1.15.8")
30-
3125
http_archive(
3226
name = "bazel_gazelle",
3327
sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
@@ -37,8 +31,26 @@ http_archive(
3731
],
3832
)
3933

34+
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
35+
4036
# gazelle:repo bazel_gazelle
41-
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
37+
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
38+
39+
# TODO(noahdietz): We should remove this eventually, it will complicate bazel dep updates.
40+
# This is necesary to include extendedops and routing annotations because rules_go doesn't
41+
# provde a recent enough version.
42+
go_repository(
43+
name = "org_golang_google_genproto",
44+
importpath = "google.golang.org/genproto",
45+
sum = "h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0=",
46+
version = "v0.0.0-20211208223120-3a66f561d7aa",
47+
)
48+
49+
go_rules_dependencies()
50+
51+
go_register_toolchains(
52+
version = "1.15.8",
53+
)
4254

4355
gazelle_dependencies()
4456

internal/gengapic/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ go_library(
4040
"@in_gopkg_yaml_v2//:yaml_v2",
4141
"@io_bazel_rules_go//proto/wkt:compiler_plugin_go_proto",
4242
"@io_bazel_rules_go//proto/wkt:descriptor_go_proto",
43+
"@org_golang_google_genproto//googleapis/cloud/extendedops",
4344
"@org_golang_google_genproto//googleapis/gapic/metadata",
4445
"@org_golang_google_protobuf//encoding/protojson",
4546
"@org_golang_google_protobuf//proto",

internal/gengapic/client_init_test.go

+28-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/googleapis/gapic-generator-go/internal/txtdiff"
2828
"google.golang.org/genproto/googleapis/api/annotations"
2929
"google.golang.org/genproto/googleapis/api/serviceconfig"
30+
"google.golang.org/genproto/googleapis/cloud/extendedops"
3031
code "google.golang.org/genproto/googleapis/rpc/code"
3132
"google.golang.org/protobuf/encoding/protojson"
3233
"google.golang.org/protobuf/proto"
@@ -348,14 +349,22 @@ func TestClientInit(t *testing.T) {
348349
Deprecated: proto.Bool(true),
349350
},
350351
}
352+
353+
opS := &descriptor.ServiceDescriptorProto{
354+
Name: proto.String("FooOperationService"),
355+
Method: []*descriptor.MethodDescriptorProto{},
356+
}
357+
358+
customOpOpts := &descriptor.MethodOptions{}
359+
proto.SetExtension(customOpOpts, extendedops.E_OperationService, opS.GetName())
351360
servCustomOp := &descriptor.ServiceDescriptorProto{
352361
Name: proto.String("Foo"),
353362
Method: []*descriptor.MethodDescriptorProto{
354363
{
355364
Name: proto.String("Zip"),
356365
InputType: proto.String(".mypackage.Bar"),
357366
OutputType: proto.String(".mypackage.Operation"),
358-
Options: &descriptor.MethodOptions{},
367+
Options: customOpOpts,
359368
},
360369
},
361370
}
@@ -368,12 +377,13 @@ func TestClientInit(t *testing.T) {
368377
}
369378

370379
for _, tst := range []struct {
371-
tstName string
372-
servName string
373-
mixins mixins
374-
serv *descriptor.ServiceDescriptorProto
375-
parameter *string
376-
imports map[pbinfo.ImportSpec]bool
380+
tstName string
381+
servName string
382+
mixins mixins
383+
serv *descriptor.ServiceDescriptorProto
384+
customOpServ *descriptor.ServiceDescriptorProto
385+
parameter *string
386+
imports map[pbinfo.ImportSpec]bool
377387
}{
378388
{
379389
tstName: "foo_client_init",
@@ -477,10 +487,11 @@ func TestClientInit(t *testing.T) {
477487
},
478488
},
479489
{
480-
tstName: "custom_op_init",
481-
servName: "",
482-
serv: servCustomOp,
483-
parameter: proto.String("go-gapic-package=path;mypackage,transport=rest,diregapic"),
490+
tstName: "custom_op_init",
491+
servName: "",
492+
serv: servCustomOp,
493+
customOpServ: opS,
494+
parameter: proto.String("go-gapic-package=path;mypackage,transport=rest,diregapic"),
484495
imports: map[pbinfo.ImportSpec]bool{
485496
{Path: "context"}: true,
486497
{Path: "fmt"}: true,
@@ -500,7 +511,7 @@ func TestClientInit(t *testing.T) {
500511
Options: &descriptor.FileOptions{
501512
GoPackage: proto.String("github.com/googleapis/mypackage/v1"),
502513
},
503-
Service: []*descriptor.ServiceDescriptorProto{tst.serv},
514+
Service: []*descriptor.ServiceDescriptorProto{tst.serv, tst.customOpServ},
504515
MessageType: []*descriptor.DescriptorProto{
505516
{
506517
Name: proto.String("Bar"),
@@ -532,6 +543,11 @@ func TestClientInit(t *testing.T) {
532543
g.aux.customOp = &customOp{
533544
message: cop,
534545
}
546+
if tst.customOpServ != nil {
547+
g.customOpServices = map[*descriptor.ServiceDescriptorProto]*descriptor.ServiceDescriptorProto{
548+
tst.serv: tst.customOpServ,
549+
}
550+
}
535551

536552
g.reset()
537553
g.makeClients(tst.serv, tst.servName)

internal/gengapic/custom_operation.go

+194-11
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ import (
1818
"fmt"
1919

2020
"github.com/golang/protobuf/protoc-gen-go/descriptor"
21+
"github.com/googleapis/gapic-generator-go/internal/pbinfo"
22+
"google.golang.org/genproto/googleapis/cloud/extendedops"
23+
"google.golang.org/protobuf/proto"
2124
)
2225

2326
// customOp represents a custom operation type for long running operations.
2427
type customOp struct {
25-
message *descriptor.DescriptorProto
26-
generated bool
28+
message *descriptor.DescriptorProto
29+
handles []*descriptor.ServiceDescriptorProto
2730
}
2831

2932
// isCustomOp determines if the given method should return a custom operation wrapper.
@@ -63,39 +66,219 @@ func (g *generator) customOpPointerType() (string, error) {
6366
// customOpInit builds a string containing the Go code for initializing the
6467
// operation wrapper type with the Go identifier for a variable that is the
6568
// proto-defined operation type.
66-
func (g *generator) customOpInit(p string) string {
69+
func (g *generator) customOpInit(h, p string) string {
6770
opName := g.aux.customOp.message.GetName()
6871

69-
s := fmt.Sprintf("&%s{proto: %s}", opName, p)
72+
s := fmt.Sprintf("&%s{&%s{c: c.operationClient, proto: %s}}", opName, h, p)
7073

7174
return s
7275
}
7376

74-
// customOperationType generates the custom operation wrapper type using the
75-
// generators current printer. This should only be called once per package.
77+
// customOperationType generates the custom operation wrapper type and operation
78+
// service handle implementations using the generators current printer. This
79+
// should only be called once per package.
7680
func (g *generator) customOperationType() error {
7781
op := g.aux.customOp
7882
if op == nil {
7983
return nil
8084
}
8185
opName := op.message.GetName()
86+
handleInt := lowerFirst(opName + "Handle")
8287

8388
ptyp, err := g.customOpPointerType()
8489
if err != nil {
8590
return err
8691
}
92+
_, opImp, err := g.descInfo.NameSpec(op.message)
93+
if err != nil {
94+
return err
95+
}
96+
g.imports[opImp] = true
97+
98+
statusField := operationField(op.message, extendedops.OperationResponseMapping_STATUS)
99+
if statusField == nil {
100+
return fmt.Errorf("operation message %s is missing an annotated status field", op.message.GetName())
101+
}
102+
103+
opNameField := operationField(op.message, extendedops.OperationResponseMapping_NAME)
104+
if opNameField == nil {
105+
return fmt.Errorf("operation message %s is missing an annotated name field", op.message.GetName())
106+
}
107+
108+
opNameGetter := fieldGetter(opNameField.GetName())
87109

88110
p := g.printf
89111

90-
p("// %s represents a long running operation for this API.", opName)
112+
p("// %s represents a long-running operation for this API.", opName)
91113
p("type %s struct {", opName)
92-
p(" proto %s", ptyp)
114+
p(" %s", handleInt)
93115
p("}")
94116
p("")
95-
p("// Proto returns the raw type this wraps.")
96-
p("func (o *%s) Proto() %s {", opName, ptyp)
97-
p(" return o.proto")
117+
118+
// Done
119+
p("// Done reports whether the long-running operation has completed.")
120+
p("func (o *%s) Done() bool {", opName)
121+
p(g.customOpStatusCheck(statusField))
98122
p("}")
123+
p("")
124+
125+
// Name
126+
p("// Name returns the name of the long-running operation.")
127+
p("// The name is assigned by the server and is unique within the service from which the operation is created.")
128+
p("func (o *%s) Name() string {", opName)
129+
p(" return o.Proto()%s", opNameGetter)
130+
p("}")
131+
p("")
132+
133+
p("type %s interface {", handleInt)
134+
p(" // Poll retrieves the operation.")
135+
p(" Poll(ctx context.Context, opts ...gax.CallOption) error")
136+
p("")
137+
p(" // Proto returns the long-running operation message.")
138+
p(" Proto() %s", ptyp)
139+
p("}")
140+
p("")
141+
g.imports[pbinfo.ImportSpec{Path: "context"}] = true
142+
g.imports[pbinfo.ImportSpec{Name: "gax", Path: "github.com/googleapis/gax-go/v2"}] = true
143+
144+
for _, handle := range op.handles {
145+
s := pbinfo.ReduceServName(handle.GetName(), opImp.Name)
146+
n := lowerFirst(s + "Handle")
147+
148+
// Look up polling method and its input.
149+
var get *descriptor.MethodDescriptorProto
150+
for _, m := range handle.GetMethod() {
151+
if m.GetName() == "Get" {
152+
get = m
153+
break
154+
}
155+
}
156+
getInput := g.descInfo.Type[get.GetInputType()]
157+
inNameField := operationResponseField(getInput.(*descriptor.DescriptorProto), opNameField.GetName())
158+
159+
// type
160+
p("// Implements the %s interface for %s.", handleInt, handle.GetName())
161+
p("type %s struct {", n)
162+
p(" c *%sClient", s)
163+
p(" proto %s", ptyp)
164+
p("}")
165+
p("")
166+
167+
// Poll
168+
p("// Poll retrieves the latest data for the long-running operation.")
169+
p("func (h *%s) Poll(ctx context.Context, opts ...gax.CallOption) error {", n)
170+
p(" resp, err := h.c.Get(ctx, &%s.%s{%s: h.proto%s}, opts...)", opImp.Name, upperFirst(getInput.GetName()), upperFirst(inNameField.GetName()), opNameGetter)
171+
p(" if err != nil {")
172+
p(" return err")
173+
p(" }")
174+
p(" h.proto = resp")
175+
p(" return nil")
176+
p("}")
177+
p("")
178+
179+
// Proto
180+
p("// Proto returns the raw type this wraps.")
181+
p("func (h *%s) Proto() %s {", n, ptyp)
182+
p(" return h.proto")
183+
p("}")
184+
p("")
185+
}
186+
187+
return nil
188+
}
189+
190+
// loadCustomOpServices maps the service declared as a google.cloud.operation_service
191+
// to the service that owns the method(s) declaring it.
192+
func (g *generator) loadCustomOpServices(servs []*descriptor.ServiceDescriptorProto) {
193+
handles := g.aux.customOp.handles
194+
for _, serv := range servs {
195+
for _, meth := range serv.GetMethod() {
196+
if opServ := g.customOpService(meth); opServ != nil {
197+
g.customOpServices[serv] = opServ
198+
if !containsService(handles, opServ) {
199+
handles = append(handles, opServ)
200+
}
201+
break
202+
}
203+
}
204+
}
205+
g.aux.customOp.handles = handles
206+
}
207+
208+
// customOpService loads the ServiceDescriptorProto for the google.cloud.operation_service
209+
// named on the given method.
210+
func (g *generator) customOpService(m *descriptor.MethodDescriptorProto) *descriptor.ServiceDescriptorProto {
211+
opServName := proto.GetExtension(m.GetOptions(), extendedops.E_OperationService).(string)
212+
if opServName == "" {
213+
return nil
214+
}
215+
216+
file := g.descInfo.ParentFile[m]
217+
fqn := fmt.Sprintf(".%s.%s", file.GetPackage(), opServName)
218+
219+
return g.descInfo.Serv[fqn]
220+
}
221+
222+
// customOpStatusCheck constructs a return statement that checks if the operation's Status
223+
// field indicates it is done.
224+
func (g *generator) customOpStatusCheck(st *descriptor.FieldDescriptorProto) string {
225+
ret := fmt.Sprintf("return o.Proto()%s", fieldGetter(st.GetName()))
226+
if st.GetType() == descriptor.FieldDescriptorProto_TYPE_ENUM {
227+
done := g.customOpStatusEnumDone()
228+
ret = fmt.Sprintf("%s == %s", ret, done)
229+
}
230+
231+
return ret
232+
}
233+
234+
// customOpStatusEnumDone constructs the Go name of the operation's status enum
235+
// DONE value.
236+
func (g *generator) customOpStatusEnumDone() string {
237+
op := g.aux.customOp.message
238+
239+
// Ignore the error here, it would failed much earlier if the
240+
// operation type was unresolvable.
241+
_, imp, _ := g.descInfo.NameSpec(op)
99242

243+
// Ignore the nil case here, it would failed earlier if the
244+
// status field was not present.
245+
statusField := operationField(op, extendedops.OperationResponseMapping_STATUS)
246+
statusEnum := g.descInfo.Type[statusField.GetTypeName()]
247+
248+
enum := fmt.Sprintf("%s_DONE", g.nestedName(g.descInfo.ParentElement[statusEnum]))
249+
250+
s := fmt.Sprintf("%s.%s", imp.Name, enum)
251+
252+
return s
253+
}
254+
255+
// operationField is a helper for loading the target google.cloud.operation_field annotation value
256+
// if present on a field in the given message.
257+
func operationField(m *descriptor.DescriptorProto, target extendedops.OperationResponseMapping) *descriptor.FieldDescriptorProto {
258+
for _, f := range m.GetField() {
259+
mapping := proto.GetExtension(f.GetOptions(), extendedops.E_OperationField).(extendedops.OperationResponseMapping)
260+
if mapping == target {
261+
return f
262+
}
263+
}
100264
return nil
101265
}
266+
267+
// operationResponseField is a helper for finding the message field that declares the target field name
268+
// in the google.cloud.operation_response_field annotation.
269+
func operationResponseField(m *descriptor.DescriptorProto, target string) *descriptor.FieldDescriptorProto {
270+
for _, f := range m.GetField() {
271+
mapping := proto.GetExtension(f.GetOptions(), extendedops.E_OperationResponseField).(string)
272+
if mapping == target {
273+
return f
274+
}
275+
}
276+
return nil
277+
}
278+
279+
// handleName is a helper for constructing a operation handle name from the
280+
// operation service name and Go package name.
281+
func handleName(s, pkg string) string {
282+
s = pbinfo.ReduceServName(s, pkg)
283+
return lowerFirst(s + "Handle")
284+
}

0 commit comments

Comments
 (0)