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

feat(gengapic): add support for AutoPopulatedFields UUID4 #1460

Merged
merged 14 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 44 additions & 0 deletions internal/gengapic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import (
"github.com/googleapis/gapic-generator-go/internal/pbinfo"
"github.com/googleapis/gapic-generator-go/internal/printer"
"github.com/googleapis/gapic-generator-go/internal/snippets"
"google.golang.org/genproto/googleapis/api/annotations"
"google.golang.org/genproto/googleapis/api/serviceconfig"
"google.golang.org/genproto/googleapis/gapic/metadata"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/runtime/protoiface"
"google.golang.org/protobuf/types/descriptorpb"
)

type generator struct {
Expand Down Expand Up @@ -302,3 +304,45 @@ func (g *generator) nestedName(nested pbinfo.ProtoType) string {

return name
}

// autoPopulatedFields returns an array of snake-case MethodDescriptorProto
// input field names that are specified for auto-population per the following
// restrictions:
//
// * The field is a top-level string field of a unary method's request message.
// * The field is not annotated with google.api.field_behavior = REQUIRED.
// * The field name is listed in google.api.publishing.method_settings.auto_populated_fields.
// * The field is annotated with google.api.field_info.format = UUID4.
func (g *generator) autoPopulatedFields(servName string, m *descriptor.MethodDescriptorProto) []string {
var apfs []string
// Find the service config's AutoPopulatedFields entry by method name.
for _, s := range g.serviceConfig.GetPublishing().GetMethodSettings() {
if s.GetSelector() == g.fqn(m) {
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
apfs = s.AutoPopulatedFields
break
}
}
inType := g.descInfo.Type[*m.InputType].(*descriptor.DescriptorProto)
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
var validated []string
for _, apf := range apfs {
var field *descriptorpb.FieldDescriptorProto
// Find the input's FieldDescriptorProto by AutoPopulatedField name.
for _, f := range inType.GetField() {
if f.GetName() == apf {
field = f
break
}
}
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
// Do nothing and continue iterating unless all conditions above are met.
switch {
case field == nil:
case field.GetType() != fieldTypeString:
case field.GetLabel() == descriptor.FieldDescriptorProto_LABEL_REQUIRED:
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
case field.GetOptions() == nil:
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
case proto.GetExtension(field.GetOptions(), annotations.E_FieldInfo).(*annotations.FieldInfo) == nil:
case proto.GetExtension(field.GetOptions(), annotations.E_FieldInfo).(*annotations.FieldInfo).Format == annotations.FieldInfo_UUID4:
validated = append(validated, field.GetName())
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
}
}
return validated
}
130 changes: 130 additions & 0 deletions internal/gengapic/generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2024 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 gengapic

import (
"testing"

"github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/google/go-cmp/cmp"
"github.com/googleapis/gapic-generator-go/internal/pbinfo"
"google.golang.org/genproto/googleapis/api/annotations"
"google.golang.org/genproto/googleapis/api/serviceconfig"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
)

func TestAutoPopulatedFields(t *testing.T) {
optsUUID4 := &descriptorpb.FieldOptions{}
proto.SetExtension(optsUUID4, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_UUID4})
optsIPV4 := &descriptorpb.FieldOptions{}
proto.SetExtension(optsIPV4, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_IPV4})
inputType := &descriptor.DescriptorProto{
Name: proto.String("InputType"),
Field: []*descriptor.FieldDescriptorProto{
{
Name: proto.String("request_id"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
Options: optsUUID4,
},
{
Name: proto.String("invalid_auto_populated_not_in_serviceconfig"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
},
{
Name: proto.String("invalid_auto_populated_no_annotation"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
},
{
Name: proto.String("invalid_auto_populated_required"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_REQUIRED),
Options: optsUUID4,
},
{
Name: proto.String("invalid_auto_populated_int"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_INT64),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
Options: optsUUID4,
},
{
Name: proto.String("invalid_auto_populated_ipv4"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
Options: optsIPV4,
},
},
}
outputType := &descriptor.DescriptorProto{
Name: proto.String("OutputType"),
}
file := &descriptor.FileDescriptorProto{
Package: proto.String("my.pkg"),
Options: &descriptor.FileOptions{
GoPackage: proto.String("mypackage"),
},
}
serv := &descriptor.ServiceDescriptorProto{
Name: proto.String("Foo"),
}

var g generator
g.opts = &options{
pkgName: "pkg",
}
g.imports = map[pbinfo.ImportSpec]bool{}

g.serviceConfig = &serviceconfig.Service{
Publishing: &annotations.Publishing{
MethodSettings: []*annotations.MethodSettings{
{
Selector: "my.pkg.Foo.GetOneThing",
AutoPopulatedFields: []string{
"request_id",
"invalid_auto_populated_no_annotation",
"invalid_auto_populated_required",
"invalid_auto_populated_int",
"invalid_auto_populated_ipv4",
},
},
},
},
}

commonTypes(&g)
g.descInfo.Type[".my.pkg."+inputType.GetName()] = inputType
g.descInfo.ParentFile[inputType] = file
g.descInfo.Type[".my.pkg."+outputType.GetName()] = outputType
g.descInfo.ParentFile[outputType] = file
g.descInfo.ParentFile[serv] = file

m := &descriptor.MethodDescriptorProto{
Name: proto.String("GetOneThing"),
InputType: proto.String(".my.pkg.InputType"),
OutputType: proto.String(".my.pkg.OutputType"),
Options: &descriptor.MethodOptions{},
}
g.descInfo.ParentElement[m] = serv
serv.Method = []*descriptor.MethodDescriptorProto{m}

got := g.autoPopulatedFields(serv.GetName(), m)
want := []string{"request_id"}
if diff := cmp.Diff(got, want); diff != "" {
t.Errorf("got(-),want(+):\n%s", diff)
}
}
24 changes: 24 additions & 0 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,30 @@ func (g *generator) insertRequestHeaders(m *descriptor.MethodDescriptorProto, t
}
}

// insertAutoPopulatedFields generates the conditional initialization of any
// default-value request fields (for the given method) that are specified for
// auto-population by autoPopulatedFields.
//
// If the field value is not equal to default value at the time of sending the
// request, implying it was set by the user, or if the field has explicit
// presence and is set by the user, the field must not be auto-populated by
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
// the client. Values automatically populated this way must be reused for
// retries of the same request.
func (g *generator) initializeAutoPopulatedFields(servName string, m *descriptor.MethodDescriptorProto) {
p := g.printf
apfs := g.autoPopulatedFields(servName, m)
if len(apfs) == 0 {
return
}
g.imports[pbinfo.ImportSpec{Path: "github.com/google/uuid"}] = true
for _, apf := range apfs {
apf = snakeToCamel(apf)
p("if req != nil && req.%s == \"\" {", apf)
p(" req.%s = uuid.New().String()", apf)
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
p("}")
}
}

func buildAccessor(field string, rawFinal bool) string {
// Corner case if passed the result of strings.Join on an empty slice.
if field == "" {
Expand Down
32 changes: 32 additions & 0 deletions internal/gengapic/gengapic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/googleapis/gapic-generator-go/internal/snippets"
"github.com/googleapis/gapic-generator-go/internal/txtdiff"
"google.golang.org/genproto/googleapis/api/annotations"
"google.golang.org/genproto/googleapis/api/serviceconfig"
metadatapb "google.golang.org/genproto/googleapis/gapic/metadata"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
Expand Down Expand Up @@ -149,6 +150,9 @@ func TestGenGRPCMethods(t *testing.T) {
Name: proto.String("NestedEnum"),
}

optsUUID4 := &descriptorpb.FieldOptions{}
proto.SetExtension(optsUUID4, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_UUID4})

inputType := &descriptor.DescriptorProto{
Name: proto.String("InputType"),
Field: []*descriptor.FieldDescriptorProto{
Expand Down Expand Up @@ -183,6 +187,12 @@ func TestGenGRPCMethods(t *testing.T) {
Type: typep(descriptor.FieldDescriptorProto_TYPE_ENUM),
TypeName: proto.String(".my.pkg.InputType.NestedEnum"),
},
{
Name: proto.String("request_id"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
quartzmo marked this conversation as resolved.
Show resolved Hide resolved
Options: optsUUID4,
},
},
EnumType: []*descriptor.EnumDescriptorProto{
nestedEnum,
Expand Down Expand Up @@ -321,6 +331,26 @@ func TestGenGRPCMethods(t *testing.T) {
"google.iam.v1.IAMPolicy": iamPolicyMethods(),
}
g.imports = map[pbinfo.ImportSpec]bool{}

g.serviceConfig = &serviceconfig.Service{
Publishing: &annotations.Publishing{
MethodSettings: []*annotations.MethodSettings{
{
Selector: "my.pkg.Foo.GetEmptyThing",
AutoPopulatedFields: []string{
"request_id",
},
},
{
Selector: "my.pkg.Foo.GetOneThing",
AutoPopulatedFields: []string{
"request_id",
},
},
},
},
}

cpb := &conf.ServiceConfig{
MethodConfig: []*conf.MethodConfig{
{
Expand Down Expand Up @@ -371,6 +401,7 @@ func TestGenGRPCMethods(t *testing.T) {
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
Expand All @@ -384,6 +415,7 @@ func TestGenGRPCMethods(t *testing.T) {
},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "net/url"}: true,
{Name: "mypackagepb", Path: "mypackage"}: true,
},
Expand Down
2 changes: 2 additions & 0 deletions internal/gengapic/gengrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func (g *generator) unaryGRPCCall(servName string, m *descriptor.MethodDescripto
lowcaseServName, m.GetName(), inSpec.Name, inType.GetName(), retTyp)

g.insertRequestHeaders(m, grpc)
g.initializeAutoPopulatedFields(servName, m)
g.appendCallOpts(m)

p("var resp *%s", retTyp)
Expand Down Expand Up @@ -142,6 +143,7 @@ func (g *generator) emptyUnaryGRPCCall(servName string, m *descriptor.MethodDesc
lowcaseServName, m.GetName(), inSpec.Name, inType.GetName())

g.insertRequestHeaders(m, grpc)
g.initializeAutoPopulatedFields(servName, m)
g.appendCallOpts(m)
p("err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {")
p(" var err error")
Expand Down
2 changes: 2 additions & 0 deletions internal/gengapic/genrest.go
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ func (g *generator) emptyUnaryRESTCall(servName string, m *descriptor.MethodDesc
p("func (c *%s) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) error {",
lowcaseServName, m.GetName(), inSpec.Name, inType.GetName())

g.initializeAutoPopulatedFields(servName, m)
// TODO(dovs): handle cancellation, metadata, osv.
// TODO(dovs): handle http headers
// TODO(dovs): handle deadlines
Expand Down Expand Up @@ -1113,6 +1114,7 @@ func (g *generator) unaryRESTCall(servName string, m *descriptor.MethodDescripto
p("func (c *%s) %s(ctx context.Context, req *%s.%s, opts ...gax.CallOption) (%s, error) {",
lowcaseServName, m.GetName(), inSpec.Name, inType.GetName(), retTyp)

g.initializeAutoPopulatedFields(servName, m)
// TODO(dovs): handle cancellation, metadata, osv.
// TODO(dovs): handle http headers
// TODO(dovs): handle deadlines?
Expand Down
38 changes: 32 additions & 6 deletions internal/gengapic/genrest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,10 +449,18 @@ func TestGenRestMethod(t *testing.T) {
Type: descriptor.FieldDescriptorProto_TYPE_STRING.Enum(),
Proto3Optional: proto.Bool(true),
}
infoOpts := &descriptorpb.FieldOptions{}
proto.SetExtension(infoOpts, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_UUID4})
requestIDField := &descriptor.FieldDescriptorProto{
Name: proto.String("request_id"),
Type: descriptor.FieldDescriptorProto_TYPE_STRING.Enum(),
Proto3Optional: proto.Bool(true),
Options: infoOpts,
}

foo := &descriptor.DescriptorProto{
Name: proto.String("Foo"),
Field: []*descriptor.FieldDescriptorProto{sizeField, otherField},
Field: []*descriptor.FieldDescriptorProto{sizeField, otherField, requestIDField},
}
foofqn := fmt.Sprintf(".%s.Foo", pkg)

Expand Down Expand Up @@ -764,9 +772,10 @@ func TestGenRestMethod(t *testing.T) {
method: emptyRPC,
options: &options{},
imports: map[pbinfo.ImportSpec]bool{
{Path: "fmt"}: true,
{Path: "google.golang.org/api/googleapi"}: true,
{Path: "net/url"}: true,
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "google.golang.org/api/googleapi"}: true,
{Path: "net/url"}: true,
{Name: "foopb", Path: "google.golang.org/genproto/cloud/foo/v1"}: true,
},
},
Expand All @@ -775,8 +784,9 @@ func TestGenRestMethod(t *testing.T) {
method: unaryRPC,
options: &options{restNumericEnum: true},
imports: map[pbinfo.ImportSpec]bool{
{Path: "bytes"}: true,
{Path: "fmt"}: true,
{Path: "bytes"}: true,
{Path: "fmt"}: true,
{Path: "github.com/google/uuid"}: true,
{Path: "google.golang.org/protobuf/encoding/protojson"}: true,
{Path: "io"}: true,
{Path: "google.golang.org/api/googleapi"}: true,
Expand Down Expand Up @@ -891,6 +901,22 @@ func TestGenRestMethod(t *testing.T) {
},
},
},
Publishing: &annotations.Publishing{
MethodSettings: []*annotations.MethodSettings{
{
Selector: "google.cloud.foo.v1.FooService.UnaryRPC",
AutoPopulatedFields: []string{
"request_id",
},
},
{
Selector: "google.cloud.foo.v1.FooService.EmptyRPC",
AutoPopulatedFields: []string{
"request_id",
},
},
},
},
}

if err := g.genRESTMethod("Foo", s, tst.method); err != nil {
Expand Down
Loading
Loading