Skip to content

Commit

Permalink
feat: support structpb.Struct as req/resp (#2632)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
codyoss authored Jun 12, 2024
1 parent 56d0d59 commit ebc44d1
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 1 deletion.
1 change: 1 addition & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
49 changes: 48 additions & 1 deletion google-api-go-generator/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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" {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down
1 change: 1 addition & 0 deletions google-api-go-generator/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestAPIs(t *testing.T) {
"json-body",
"mapofany",
"mapofarrayofobjects",
"mapprotostruct",
"mapofint64strings",
"mapofobjects",
"mapofstrings-1",
Expand Down
42 changes: 42 additions & 0 deletions google-api-go-generator/testdata/mapprotostruct.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
235 changes: 235 additions & 0 deletions google-api-go-generator/testdata/mapprotostruct.want
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit ebc44d1

Please sign in to comment.