diff --git a/internal/gengapic/generator.go b/internal/gengapic/generator.go index 755ba49ab..5cd539965 100644 --- a/internal/gengapic/generator.go +++ b/internal/gengapic/generator.go @@ -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" @@ -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 +} diff --git a/internal/gengapic/generator_test.go b/internal/gengapic/generator_test.go new file mode 100644 index 000000000..c193952c1 --- /dev/null +++ b/internal/gengapic/generator_test.go @@ -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) + } +} diff --git a/internal/gengapic/gengapic.go b/internal/gengapic/gengapic.go index 2dad3446a..9afe29fae 100644 --- a/internal/gengapic/gengapic.go +++ b/internal/gengapic/gengapic.go @@ -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 == "" { diff --git a/internal/gengapic/gengapic_test.go b/internal/gengapic/gengapic_test.go index de0c04af6..35b1fc34c 100644 --- a/internal/gengapic/gengapic_test.go +++ b/internal/gengapic/gengapic_test.go @@ -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" @@ -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{ @@ -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, @@ -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{ { @@ -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, }, @@ -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, }, diff --git a/internal/gengapic/gengrpc.go b/internal/gengapic/gengrpc.go index 922ab59b0..efff793a2 100644 --- a/internal/gengapic/gengrpc.go +++ b/internal/gengapic/gengrpc.go @@ -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) @@ -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") diff --git a/internal/gengapic/genrest.go b/internal/gengapic/genrest.go index e0c3c7983..6ce417356 100644 --- a/internal/gengapic/genrest.go +++ b/internal/gengapic/genrest.go @@ -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 @@ -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? diff --git a/internal/gengapic/genrest_test.go b/internal/gengapic/genrest_test.go index f5d1d126c..f28b009e0 100644 --- a/internal/gengapic/genrest_test.go +++ b/internal/gengapic/genrest_test.go @@ -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) @@ -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, }, }, @@ -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, @@ -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 { diff --git a/internal/gengapic/helpers.go b/internal/gengapic/helpers.go index 864fffa42..464bb9d01 100644 --- a/internal/gengapic/helpers.go +++ b/internal/gengapic/helpers.go @@ -110,15 +110,21 @@ func grpcClientField(reducedServName string) string { return lowerFirst(reducedServName + "Client") } -// hasField returns true if the target DescriptorProto has the given field, -// otherwise it returns false. -func hasField(m *descriptor.DescriptorProto, field string) bool { +// getField returns a FieldDescriptorProto pointer if the target +// DescriptorProto has the given field, otherwise it returns nil. +func getField(m *descriptor.DescriptorProto, field string) *descriptor.FieldDescriptorProto { for _, f := range m.GetField() { if f.GetName() == field { - return true + return f } } - return false + return nil +} + +// hasField returns true if the target DescriptorProto has the given field, +// otherwise it returns false. +func hasField(m *descriptor.DescriptorProto, field string) bool { + return getField(m, field) != nil } // hasMethod reports if the given service defines an RPC with the same name as diff --git a/internal/gengapic/testdata/method_GetEmptyThing.want b/internal/gengapic/testdata/method_GetEmptyThing.want index 756cc49fe..eaf6c423f 100644 --- a/internal/gengapic/testdata/method_GetEmptyThing.want +++ b/internal/gengapic/testdata/method_GetEmptyThing.want @@ -3,6 +3,9 @@ func (c *fooGRPCClient) GetEmptyThing(ctx context.Context, req *mypackagepb.Inpu hds = append(c.xGoogHeaders, hds...) ctx = gax.InsertMetadataIntoOutgoingContext(ctx, hds...) + if req != nil && req.RequestId == nil { + req.RequestId = proto.String(uuid.NewString()) + } opts = append((*c.CallOptions).GetEmptyThing[0:len((*c.CallOptions).GetEmptyThing):len((*c.CallOptions).GetEmptyThing)], opts...) err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { var err error diff --git a/internal/gengapic/testdata/method_GetOneThing.want b/internal/gengapic/testdata/method_GetOneThing.want index ac0ba44fd..54ba5d7ea 100644 --- a/internal/gengapic/testdata/method_GetOneThing.want +++ b/internal/gengapic/testdata/method_GetOneThing.want @@ -3,6 +3,12 @@ func (c *fooGRPCClient) GetOneThing(ctx context.Context, req *mypackagepb.InputT hds = append(c.xGoogHeaders, hds...) ctx = gax.InsertMetadataIntoOutgoingContext(ctx, hds...) + if req != nil && req.RequestId == nil { + req.RequestId = proto.String(uuid.NewString()) + } + if req != nil && req.GetNonProto3optionalRequestId() == "" { + req.NonProto3optionalRequestId = uuid.NewString() + } opts = append((*c.CallOptions).GetOneThing[0:len((*c.CallOptions).GetOneThing):len((*c.CallOptions).GetOneThing)], opts...) var resp *mypackagepb.OutputType err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error { diff --git a/internal/gengapic/testdata/rest_CustomOp.want b/internal/gengapic/testdata/rest_CustomOp.want index 7b0ceccb7..faa858f72 100644 --- a/internal/gengapic/testdata/rest_CustomOp.want +++ b/internal/gengapic/testdata/rest_CustomOp.want @@ -9,6 +9,9 @@ func (c *fooRESTClient) CustomOp(ctx context.Context, req *foopb.Foo, opts ...ga if req != nil && req.Other != nil { params.Add("other", fmt.Sprintf("%v", req.GetOther())) } + if req != nil && req.RequestId != nil { + params.Add("requestId", fmt.Sprintf("%v", req.GetRequestId())) + } params.Add("size", fmt.Sprintf("%v", req.GetSize())) baseUrl.RawQuery = params.Encode() diff --git a/internal/gengapic/testdata/rest_EmptyRPC.want b/internal/gengapic/testdata/rest_EmptyRPC.want index a3cde5014..1cdcc023b 100644 --- a/internal/gengapic/testdata/rest_EmptyRPC.want +++ b/internal/gengapic/testdata/rest_EmptyRPC.want @@ -1,4 +1,7 @@ func (c *fooRESTClient) EmptyRPC(ctx context.Context, req *foopb.Foo, opts ...gax.CallOption) error { + if req != nil && req.RequestId == nil { + req.RequestId = proto.String(uuid.NewString()) + } baseUrl, err := url.Parse(c.endpoint) if err != nil { return err @@ -6,6 +9,9 @@ func (c *fooRESTClient) EmptyRPC(ctx context.Context, req *foopb.Foo, opts ...ga baseUrl.Path += fmt.Sprintf("/v1/foo/%v", req.GetOther()) params := url.Values{} + if req != nil && req.RequestId != nil { + params.Add("requestId", fmt.Sprintf("%v", req.GetRequestId())) + } params.Add("size", fmt.Sprintf("%v", req.GetSize())) baseUrl.RawQuery = params.Encode() diff --git a/internal/gengapic/testdata/rest_UnaryRPC.want b/internal/gengapic/testdata/rest_UnaryRPC.want index 625f36a9e..473af58df 100644 --- a/internal/gengapic/testdata/rest_UnaryRPC.want +++ b/internal/gengapic/testdata/rest_UnaryRPC.want @@ -1,4 +1,7 @@ func (c *fooRESTClient) UnaryRPC(ctx context.Context, req *foopb.Foo, opts ...gax.CallOption) (*foopb.Foo, error) { + if req != nil && req.RequestId == nil { + req.RequestId = proto.String(uuid.NewString()) + } m := protojson.MarshalOptions{AllowPartial: true, UseEnumNumbers: true} jsonReq, err := m.Marshal(req) if err != nil { diff --git a/rules_go_gapic/go_gapic.bzl b/rules_go_gapic/go_gapic.bzl index f1a52d992..b32c2dccc 100644 --- a/rules_go_gapic/go_gapic.bzl +++ b/rules_go_gapic/go_gapic.bzl @@ -154,6 +154,7 @@ def go_gapic_library( ) actual_deps = deps + [ + "@com_github_google_uuid//:go_default_library", "@com_github_googleapis_gax_go_v2//:go_default_library", "@com_github_googleapis_gax_go_v2//apierror:go_default_library", "@org_golang_google_api//googleapi:go_default_library",