From ebc44d15955116f5647020142005c34e6a8d1d47 Mon Sep 17 00:00:00 2001 From: Cody Oss <6331106+codyoss@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:32:21 -0500 Subject: [PATCH] feat: support structpb.Struct as req/resp (#2632) There are some APIs that have started to use this type for the request and/or respsonse. Similar to how we had to specially handle protos HTTP body we need a good translation for this type as well. `map[string]any` seemed like the best fit as that is the input needed to create a `Struct`. The other choice would have been a `googleapis.RawMessage`. RawMessage is used today when a field would be of type Struct, but this is a less convient type, and less precise type, to use than a map directly. Fixes: #2601 --- go.work.sum | 1 + google-api-go-generator/gen.go | 49 +++- google-api-go-generator/gen_test.go | 1 + .../testdata/mapprotostruct.json | 42 ++++ .../testdata/mapprotostruct.want | 235 ++++++++++++++++++ 5 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 google-api-go-generator/testdata/mapprotostruct.json create mode 100644 google-api-go-generator/testdata/mapprotostruct.want diff --git a/go.work.sum b/go.work.sum index 0f65fa8d51b..e5f9fd34ea4 100644 --- a/go.work.sum +++ b/go.work.sum @@ -32,6 +32,7 @@ cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzc cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute v1.27.0 h1:EGawh2RUnfHT5g8f/FX3Ds6KZuIBC77hZoDrBvEZw94= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI= cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA= diff --git a/google-api-go-generator/gen.go b/google-api-go-generator/gen.go index 9216cc38ad0..6d8d6debbca 100644 --- a/google-api-go-generator/gen.go +++ b/google-api-go-generator/gen.go @@ -1971,6 +1971,8 @@ func (meth *Method) generateCode() { retType := responseType(a, meth.m) if meth.IsRawResponse() { retType = "*http.Response" + } else if meth.IsProtoStructResponse() { + retType = "map[string]any" } retTypeComma := retType if retTypeComma != "" { @@ -2247,6 +2249,10 @@ func (meth *Method) generateCode() { pn("var body io.Reader = nil") if meth.IsRawRequest() { pn("body = c.body_") + } else if meth.IsProtoStructRequest() { + pn("protoBytes, err := json.Marshal(c.req)") + pn("if err != nil { return nil, err }") + pn("body = bytes.NewReader(protoBytes)") } else { if ba := args.bodyArg(); ba != nil && httpMethod != "GET" { if meth.m.ID == "ml.projects.predict" { @@ -2384,7 +2390,9 @@ func (meth *Method) generateCode() { if retTypeComma == "" { pn("return nil") } else { - if mapRetType { + if meth.IsProtoStructResponse() { + pn("var ret map[string]any") + } else if mapRetType { pn("var ret %s", responseType(a, meth.m)) } else { pn("ret := &%s{", responseTypeLiteral(a, meth.m)) @@ -2529,6 +2537,40 @@ func (meth *Method) IsRawRequest() bool { return meth.m.Request.Ref == "HttpBody" } +// IsProtoStructRequest determines if the method request type is a +// [google.golang.org/protobuf/types/known/structpb.Struct]. +func (meth *Method) IsProtoStructRequest() bool { + if meth == nil || meth.m == nil { + return false + } + + return isProtoStruct(meth.m.Request) +} + +// IsProtoStructResponse determines if the method response type is a +// [google.golang.org/protobuf/types/known/structpb.Struct]. +func (meth *Method) IsProtoStructResponse() bool { + if meth == nil || meth.m == nil { + return false + } + + return isProtoStruct(meth.m.Response) +} + +// isProtoStruct determines if the Schema represents a +// [google.golang.org/protobuf/types/known/structpb.Struct]. +func isProtoStruct(s *disco.Schema) bool { + if s == nil { + return false + } + + if s.Ref == "GoogleProtobufStruct" { + return true + } + + return false +} + func (meth *Method) IsRawResponse() bool { if meth.m.Response == nil { return false @@ -2567,6 +2609,11 @@ func (meth *Method) NewArguments() *arguments { goname: "body_", gotype: "io.Reader", }) + } else if meth.IsProtoStructRequest() { + args.AddArg(&argument{ + goname: "req", + gotype: "map[string]any", + }) } else { args.AddArg(meth.NewBodyArg(rs)) } diff --git a/google-api-go-generator/gen_test.go b/google-api-go-generator/gen_test.go index cb798a3a6e2..a9ad03a2823 100644 --- a/google-api-go-generator/gen_test.go +++ b/google-api-go-generator/gen_test.go @@ -39,6 +39,7 @@ func TestAPIs(t *testing.T) { "json-body", "mapofany", "mapofarrayofobjects", + "mapprotostruct", "mapofint64strings", "mapofobjects", "mapofstrings-1", diff --git a/google-api-go-generator/testdata/mapprotostruct.json b/google-api-go-generator/testdata/mapprotostruct.json new file mode 100644 index 00000000000..ee9bcd6230d --- /dev/null +++ b/google-api-go-generator/testdata/mapprotostruct.json @@ -0,0 +1,42 @@ +{ + "kind": "discovery#restDescription", + "etag": "\"kEk3sFj6Ef5_yR1-H3bAO6qw9mI/3m5rB86FE5KuW1K3jAl88AxCreg\"", + "discoveryVersion": "v1", + "id": "mapprotostruct:v1", + "name": "mapprotostruct", + "version": "v1", + "title": "Example API", + "description": "The Example API demonstrates handling structpb.Struct.", + "ownerDomain": "google.com", + "ownerName": "Google", + "protocol": "rest", + "schemas": { + "GoogleProtobufStruct": { + "id": "GoogleProtobufStruct", + "description": "`Struct` represents a structured data value, consisting of fields which map to dynamically typed values. In some languages, `Struct` might be supported by a native representation. For example, in scripting languages like JS a struct is represented as an object. The details of that representation are described together with the proto support for the language. The JSON representation for `Struct` is JSON object.", + "type": "object", + "additionalProperties": { + "type": "any", + "description": "Properties of the object." + } + } + }, + "resources": { + "atlas": { + "methods": { + "getMap": { + "id": "mapprotostruct.getMap", + "path": "map", + "httpMethod": "GET", + "description": "Get a map.", + "request": { + "$ref": "GoogleProtobufStruct" + }, + "response": { + "$ref": "GoogleProtobufStruct" + } + } + } + } + } +} diff --git a/google-api-go-generator/testdata/mapprotostruct.want b/google-api-go-generator/testdata/mapprotostruct.want new file mode 100644 index 00000000000..74118a411a2 --- /dev/null +++ b/google-api-go-generator/testdata/mapprotostruct.want @@ -0,0 +1,235 @@ +// Copyright YEAR Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Code generated file. DO NOT EDIT. + +// Package mapprotostruct provides access to the Example API. +// +// # Library status +// +// These client libraries are officially supported by Google. However, this +// library is considered complete and is in maintenance mode. This means +// that we will address critical bugs and security issues but will not add +// any new features. +// +// When possible, we recommend using our newer +// [Cloud Client Libraries for Go](https://pkg.go.dev/cloud.google.com/go) +// that are still actively being worked and iterated on. +// +// # Creating a client +// +// Usage example: +// +// import "google.golang.org/api/mapprotostruct/v1" +// ... +// ctx := context.Background() +// mapprotostructService, err := mapprotostruct.NewService(ctx) +// +// In this example, Google Application Default Credentials are used for +// authentication. For information on how to create and obtain Application +// Default Credentials, see https://developers.google.com/identity/protocols/application-default-credentials. +// +// # Other authentication options +// +// To use an API key for authentication (note: some APIs do not support API +// keys), use [google.golang.org/api/option.WithAPIKey]: +// +// mapprotostructService, err := mapprotostruct.NewService(ctx, option.WithAPIKey("AIza...")) +// +// To use an OAuth token (e.g., a user token obtained via a three-legged OAuth +// flow, use [google.golang.org/api/option.WithTokenSource]: +// +// config := &oauth2.Config{...} +// // ... +// token, err := config.Exchange(ctx, ...) +// mapprotostructService, err := mapprotostruct.NewService(ctx, option.WithTokenSource(config.TokenSource(ctx, token))) +// +// See [google.golang.org/api/option.ClientOption] for details on options. +package mapprotostruct // import "google.golang.org/api/mapprotostruct/v1" + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + + googleapi "google.golang.org/api/googleapi" + internal "google.golang.org/api/internal" + gensupport "google.golang.org/api/internal/gensupport" + option "google.golang.org/api/option" + internaloption "google.golang.org/api/option/internaloption" + htransport "google.golang.org/api/transport/http" +) + +// Always reference these packages, just in case the auto-generated code +// below doesn't. +var _ = bytes.NewBuffer +var _ = strconv.Itoa +var _ = fmt.Sprintf +var _ = json.NewDecoder +var _ = io.Copy +var _ = url.Parse +var _ = gensupport.MarshalJSON +var _ = googleapi.Version +var _ = errors.New +var _ = strings.Replace +var _ = context.Canceled +var _ = internaloption.WithDefaultEndpoint +var _ = internal.Version + +const apiId = "mapprotostruct:v1" +const apiName = "mapprotostruct" +const apiVersion = "v1" +const basePath = "https://www.googleapis.com/discovery/v1/apis" +const basePathTemplate = "https://www.UNIVERSE_DOMAIN/discovery/v1/apis" + +// NewService creates a new Service. +func NewService(ctx context.Context, opts ...option.ClientOption) (*Service, error) { + opts = append(opts, internaloption.WithDefaultEndpoint(basePath)) + opts = append(opts, internaloption.WithDefaultEndpointTemplate(basePathTemplate)) + opts = append(opts, internaloption.EnableNewAuthLibrary()) + client, endpoint, err := htransport.NewClient(ctx, opts...) + if err != nil { + return nil, err + } + s, err := New(client) + if err != nil { + return nil, err + } + if endpoint != "" { + s.BasePath = endpoint + } + return s, nil +} + +// New creates a new Service. It uses the provided http.Client for requests. +// +// Deprecated: please use NewService instead. +// To provide a custom HTTP client, use option.WithHTTPClient. +// If you are using google.golang.org/api/googleapis/transport.APIKey, use option.WithAPIKey with NewService instead. +func New(client *http.Client) (*Service, error) { + if client == nil { + return nil, errors.New("client is nil") + } + s := &Service{client: client, BasePath: basePath} + s.Atlas = NewAtlasService(s) + return s, nil +} + +type Service struct { + client *http.Client + BasePath string // API endpoint base URL + UserAgent string // optional additional User-Agent fragment + + Atlas *AtlasService +} + +func (s *Service) userAgent() string { + if s.UserAgent == "" { + return googleapi.UserAgent + } + return googleapi.UserAgent + " " + s.UserAgent +} + +func NewAtlasService(s *Service) *AtlasService { + rs := &AtlasService{s: s} + return rs +} + +type AtlasService struct { + s *Service +} + +type AtlasGetMapCall struct { + s *Service + req map[string]any + urlParams_ gensupport.URLParams + ifNoneMatch_ string + ctx_ context.Context + header_ http.Header +} + +// GetMap: Get a map. +func (r *AtlasService) GetMap(req map[string]any) *AtlasGetMapCall { + c := &AtlasGetMapCall{s: r.s, urlParams_: make(gensupport.URLParams)} + c.req = req + return c +} + +// Fields allows partial responses to be retrieved. See +// https://developers.google.com/gdata/docs/2.0/basics#PartialResponse for more +// details. +func (c *AtlasGetMapCall) Fields(s ...googleapi.Field) *AtlasGetMapCall { + c.urlParams_.Set("fields", googleapi.CombineFields(s)) + return c +} + +// IfNoneMatch sets an optional parameter which makes the operation fail if the +// object's ETag matches the given value. This is useful for getting updates +// only after the object has changed since the last request. +func (c *AtlasGetMapCall) IfNoneMatch(entityTag string) *AtlasGetMapCall { + c.ifNoneMatch_ = entityTag + return c +} + +// Context sets the context to be used in this call's Do method. +func (c *AtlasGetMapCall) Context(ctx context.Context) *AtlasGetMapCall { + c.ctx_ = ctx + return c +} + +// Header returns a http.Header that can be modified by the caller to add +// headers to the request. +func (c *AtlasGetMapCall) Header() http.Header { + if c.header_ == nil { + c.header_ = make(http.Header) + } + return c.header_ +} + +func (c *AtlasGetMapCall) doRequest(alt string) (*http.Response, error) { + reqHeaders := gensupport.SetHeaders(c.s.userAgent(), "", c.header_) + if c.ifNoneMatch_ != "" { + reqHeaders.Set("If-None-Match", c.ifNoneMatch_) + } + var body io.Reader = nil + protoBytes, err := json.Marshal(c.req) + if err != nil { + return nil, err + } + body = bytes.NewReader(protoBytes) + urls := googleapi.ResolveRelative(c.s.BasePath, "map") + urls += "?" + c.urlParams_.Encode() + req, err := http.NewRequest("GET", urls, body) + if err != nil { + return nil, err + } + req.Header = reqHeaders + return gensupport.SendRequest(c.ctx_, c.s.client, req) +} + +// Do executes the "mapprotostruct.getMap" call. +func (c *AtlasGetMapCall) Do(opts ...googleapi.CallOption) (map[string]any, error) { + gensupport.SetOptions(c.urlParams_, opts...) + res, err := c.doRequest("json") + if err != nil { + return nil, err + } + defer googleapi.CloseBody(res) + if err := googleapi.CheckResponse(res); err != nil { + return nil, gensupport.WrapError(err) + } + var ret map[string]any + target := &ret + if err := gensupport.DecodeResponse(target, res); err != nil { + return nil, err + } + return ret, nil +}