Skip to content

Commit

Permalink
feat(gengapic): add support for AutoPopulatedFields UUID4 (#1460)
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo authored Feb 20, 2024
1 parent 2f1a8d3 commit 2f3b7b9
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 11 deletions.
35 changes: 35 additions & 0 deletions internal/gengapic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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"
Expand Down Expand Up @@ -302,3 +303,37 @@ func (g *generator) nestedName(nested pbinfo.ProtoType) string {

return name
}

// autoPopulatedFields returns an array of FieldDescriptorProto pointers for the
// given MethodDescriptorProto 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) []*descriptor.FieldDescriptorProto {
var apfs []string
// Find the service config's AutoPopulatedFields entry by method name.
mfqn := g.fqn(m)
for _, s := range g.serviceConfig.GetPublishing().GetMethodSettings() {
if s.GetSelector() == mfqn {
apfs = s.AutoPopulatedFields
break
}
}
inType := g.descInfo.Type[m.GetInputType()].(*descriptor.DescriptorProto)
var validated []*descriptor.FieldDescriptorProto
for _, apf := range apfs {
field := getField(inType, apf)
// Do nothing and continue iterating unless all conditions above are met.
switch {
case field == nil:
case field.GetType() != fieldTypeString:
case isRequired(field):
case proto.GetExtension(field.GetOptions(), annotations.E_FieldInfo).(*annotations.FieldInfo).GetFormat() == annotations.FieldInfo_UUID4:
validated = append(validated, field)
}
}
return validated
}
140 changes: 140 additions & 0 deletions internal/gengapic/generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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/testing/protocmp"
"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})

optsRequiredAndUUID4 := &descriptorpb.FieldOptions{}
proto.SetExtension(optsRequiredAndUUID4, annotations.E_FieldBehavior, []annotations.FieldBehavior{annotations.FieldBehavior_REQUIRED})
proto.SetExtension(optsRequiredAndUUID4, annotations.E_FieldInfo, &annotations.FieldInfo{Format: annotations.FieldInfo_UUID4})

requestIDField := &descriptor.FieldDescriptorProto{
Name: proto.String("request_id"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
Proto3Optional: proto.Bool(true),
Options: optsUUID4,
}
inputType := &descriptor.DescriptorProto{
Name: proto.String("InputType"),
Field: []*descriptor.FieldDescriptorProto{
requestIDField,
{
Name: proto.String("invalid_auto_populated_not_in_serviceconfig"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
},
{
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: optsRequiredAndUUID4,
},
{
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 := []*descriptorpb.FieldDescriptorProto{requestIDField}
if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" {
t.Errorf("got(-),want(+):\n%s", diff)
}
}
31 changes: 31 additions & 0 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,37 @@ 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
// 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) {
apfs := g.autoPopulatedFields(servName, m)
if len(apfs) == 0 {
return
}
g.imports[pbinfo.ImportSpec{Path: "github.com/google/uuid"}] = true
p := g.printf
for _, apf := range apfs {
f := buildAccessor(apf.GetName(), true)
if apf.GetProto3Optional() {
// Type will be *string if field has explicit presence.
p("if req != nil && req%s == nil {", f)
p(" req%s = proto.String(uuid.NewString())", f)
} else {
// Type will be string if field does not have explicit presence.
p(`if req != nil && req%s == "" {`, buildAccessor(apf.GetName(), false))
p(" req%s = uuid.NewString()", f)
}
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
40 changes: 40 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,19 @@ 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),
Proto3Optional: proto.Bool(true),
Options: optsUUID4,
},
{
Name: proto.String("non_proto3optional_request_id"),
Type: typep(descriptor.FieldDescriptorProto_TYPE_STRING),
Label: labelp(descriptor.FieldDescriptorProto_LABEL_OPTIONAL),
Options: optsUUID4,
},
},
EnumType: []*descriptor.EnumDescriptorProto{
nestedEnum,
Expand Down Expand Up @@ -321,6 +338,27 @@ 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",
"non_proto3optional_request_id",
},
},
},
},
}

cpb := &conf.ServiceConfig{
MethodConfig: []*conf.MethodConfig{
{
Expand Down Expand Up @@ -371,6 +409,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 +423,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
Loading

0 comments on commit 2f3b7b9

Please sign in to comment.