Skip to content

Commit ebc44d1

Browse files
authored
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
1 parent 56d0d59 commit ebc44d1

File tree

5 files changed

+327
-1
lines changed

5 files changed

+327
-1
lines changed

go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzc
3232
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
3333
cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40=
3434
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
35+
cloud.google.com/go/compute v1.27.0 h1:EGawh2RUnfHT5g8f/FX3Ds6KZuIBC77hZoDrBvEZw94=
3536
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
3637
cloud.google.com/go/contactcenterinsights v1.13.0/go.mod h1:ieq5d5EtHsu8vhe2y3amtZ+BE+AQwX5qAy7cpo0POsI=
3738
cloud.google.com/go/container v1.31.0/go.mod h1:7yABn5s3Iv3lmw7oMmyGbeV6tQj86njcTijkkGuvdZA=

google-api-go-generator/gen.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,8 @@ func (meth *Method) generateCode() {
19711971
retType := responseType(a, meth.m)
19721972
if meth.IsRawResponse() {
19731973
retType = "*http.Response"
1974+
} else if meth.IsProtoStructResponse() {
1975+
retType = "map[string]any"
19741976
}
19751977
retTypeComma := retType
19761978
if retTypeComma != "" {
@@ -2247,6 +2249,10 @@ func (meth *Method) generateCode() {
22472249
pn("var body io.Reader = nil")
22482250
if meth.IsRawRequest() {
22492251
pn("body = c.body_")
2252+
} else if meth.IsProtoStructRequest() {
2253+
pn("protoBytes, err := json.Marshal(c.req)")
2254+
pn("if err != nil { return nil, err }")
2255+
pn("body = bytes.NewReader(protoBytes)")
22502256
} else {
22512257
if ba := args.bodyArg(); ba != nil && httpMethod != "GET" {
22522258
if meth.m.ID == "ml.projects.predict" {
@@ -2384,7 +2390,9 @@ func (meth *Method) generateCode() {
23842390
if retTypeComma == "" {
23852391
pn("return nil")
23862392
} else {
2387-
if mapRetType {
2393+
if meth.IsProtoStructResponse() {
2394+
pn("var ret map[string]any")
2395+
} else if mapRetType {
23882396
pn("var ret %s", responseType(a, meth.m))
23892397
} else {
23902398
pn("ret := &%s{", responseTypeLiteral(a, meth.m))
@@ -2529,6 +2537,40 @@ func (meth *Method) IsRawRequest() bool {
25292537
return meth.m.Request.Ref == "HttpBody"
25302538
}
25312539

2540+
// IsProtoStructRequest determines if the method request type is a
2541+
// [google.golang.org/protobuf/types/known/structpb.Struct].
2542+
func (meth *Method) IsProtoStructRequest() bool {
2543+
if meth == nil || meth.m == nil {
2544+
return false
2545+
}
2546+
2547+
return isProtoStruct(meth.m.Request)
2548+
}
2549+
2550+
// IsProtoStructResponse determines if the method response type is a
2551+
// [google.golang.org/protobuf/types/known/structpb.Struct].
2552+
func (meth *Method) IsProtoStructResponse() bool {
2553+
if meth == nil || meth.m == nil {
2554+
return false
2555+
}
2556+
2557+
return isProtoStruct(meth.m.Response)
2558+
}
2559+
2560+
// isProtoStruct determines if the Schema represents a
2561+
// [google.golang.org/protobuf/types/known/structpb.Struct].
2562+
func isProtoStruct(s *disco.Schema) bool {
2563+
if s == nil {
2564+
return false
2565+
}
2566+
2567+
if s.Ref == "GoogleProtobufStruct" {
2568+
return true
2569+
}
2570+
2571+
return false
2572+
}
2573+
25322574
func (meth *Method) IsRawResponse() bool {
25332575
if meth.m.Response == nil {
25342576
return false
@@ -2567,6 +2609,11 @@ func (meth *Method) NewArguments() *arguments {
25672609
goname: "body_",
25682610
gotype: "io.Reader",
25692611
})
2612+
} else if meth.IsProtoStructRequest() {
2613+
args.AddArg(&argument{
2614+
goname: "req",
2615+
gotype: "map[string]any",
2616+
})
25702617
} else {
25712618
args.AddArg(meth.NewBodyArg(rs))
25722619
}

google-api-go-generator/gen_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func TestAPIs(t *testing.T) {
3939
"json-body",
4040
"mapofany",
4141
"mapofarrayofobjects",
42+
"mapprotostruct",
4243
"mapofint64strings",
4344
"mapofobjects",
4445
"mapofstrings-1",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"kind": "discovery#restDescription",
3+
"etag": "\"kEk3sFj6Ef5_yR1-H3bAO6qw9mI/3m5rB86FE5KuW1K3jAl88AxCreg\"",
4+
"discoveryVersion": "v1",
5+
"id": "mapprotostruct:v1",
6+
"name": "mapprotostruct",
7+
"version": "v1",
8+
"title": "Example API",
9+
"description": "The Example API demonstrates handling structpb.Struct.",
10+
"ownerDomain": "google.com",
11+
"ownerName": "Google",
12+
"protocol": "rest",
13+
"schemas": {
14+
"GoogleProtobufStruct": {
15+
"id": "GoogleProtobufStruct",
16+
"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.",
17+
"type": "object",
18+
"additionalProperties": {
19+
"type": "any",
20+
"description": "Properties of the object."
21+
}
22+
}
23+
},
24+
"resources": {
25+
"atlas": {
26+
"methods": {
27+
"getMap": {
28+
"id": "mapprotostruct.getMap",
29+
"path": "map",
30+
"httpMethod": "GET",
31+
"description": "Get a map.",
32+
"request": {
33+
"$ref": "GoogleProtobufStruct"
34+
},
35+
"response": {
36+
"$ref": "GoogleProtobufStruct"
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright YEAR Google LLC.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Code generated file. DO NOT EDIT.
6+
7+
// Package mapprotostruct provides access to the Example API.
8+
//
9+
// # Library status
10+
//
11+
// These client libraries are officially supported by Google. However, this
12+
// library is considered complete and is in maintenance mode. This means
13+
// that we will address critical bugs and security issues but will not add
14+
// any new features.
15+
//
16+
// When possible, we recommend using our newer
17+
// [Cloud Client Libraries for Go](https://pkg.go.dev/cloud.google.com/go)
18+
// that are still actively being worked and iterated on.
19+
//
20+
// # Creating a client
21+
//
22+
// Usage example:
23+
//
24+
// import "google.golang.org/api/mapprotostruct/v1"
25+
// ...
26+
// ctx := context.Background()
27+
// mapprotostructService, err := mapprotostruct.NewService(ctx)
28+
//
29+
// In this example, Google Application Default Credentials are used for
30+
// authentication. For information on how to create and obtain Application
31+
// Default Credentials, see https://developers.google.com/identity/protocols/application-default-credentials.
32+
//
33+
// # Other authentication options
34+
//
35+
// To use an API key for authentication (note: some APIs do not support API
36+
// keys), use [google.golang.org/api/option.WithAPIKey]:
37+
//
38+
// mapprotostructService, err := mapprotostruct.NewService(ctx, option.WithAPIKey("AIza..."))
39+
//
40+
// To use an OAuth token (e.g., a user token obtained via a three-legged OAuth
41+
// flow, use [google.golang.org/api/option.WithTokenSource]:
42+
//
43+
// config := &oauth2.Config{...}
44+
// // ...
45+
// token, err := config.Exchange(ctx, ...)
46+
// mapprotostructService, err := mapprotostruct.NewService(ctx, option.WithTokenSource(config.TokenSource(ctx, token)))
47+
//
48+
// See [google.golang.org/api/option.ClientOption] for details on options.
49+
package mapprotostruct // import "google.golang.org/api/mapprotostruct/v1"
50+
51+
import (
52+
"bytes"
53+
"context"
54+
"encoding/json"
55+
"errors"
56+
"fmt"
57+
"io"
58+
"net/http"
59+
"net/url"
60+
"strconv"
61+
"strings"
62+
63+
googleapi "google.golang.org/api/googleapi"
64+
internal "google.golang.org/api/internal"
65+
gensupport "google.golang.org/api/internal/gensupport"
66+
option "google.golang.org/api/option"
67+
internaloption "google.golang.org/api/option/internaloption"
68+
htransport "google.golang.org/api/transport/http"
69+
)
70+
71+
// Always reference these packages, just in case the auto-generated code
72+
// below doesn't.
73+
var _ = bytes.NewBuffer
74+
var _ = strconv.Itoa
75+
var _ = fmt.Sprintf
76+
var _ = json.NewDecoder
77+
var _ = io.Copy
78+
var _ = url.Parse
79+
var _ = gensupport.MarshalJSON
80+
var _ = googleapi.Version
81+
var _ = errors.New
82+
var _ = strings.Replace
83+
var _ = context.Canceled
84+
var _ = internaloption.WithDefaultEndpoint
85+
var _ = internal.Version
86+
87+
const apiId = "mapprotostruct:v1"
88+
const apiName = "mapprotostruct"
89+
const apiVersion = "v1"
90+
const basePath = "https://www.googleapis.com/discovery/v1/apis"
91+
const basePathTemplate = "https://www.UNIVERSE_DOMAIN/discovery/v1/apis"
92+
93+
// NewService creates a new Service.
94+
func NewService(ctx context.Context, opts ...option.ClientOption) (*Service, error) {
95+
opts = append(opts, internaloption.WithDefaultEndpoint(basePath))
96+
opts = append(opts, internaloption.WithDefaultEndpointTemplate(basePathTemplate))
97+
opts = append(opts, internaloption.EnableNewAuthLibrary())
98+
client, endpoint, err := htransport.NewClient(ctx, opts...)
99+
if err != nil {
100+
return nil, err
101+
}
102+
s, err := New(client)
103+
if err != nil {
104+
return nil, err
105+
}
106+
if endpoint != "" {
107+
s.BasePath = endpoint
108+
}
109+
return s, nil
110+
}
111+
112+
// New creates a new Service. It uses the provided http.Client for requests.
113+
//
114+
// Deprecated: please use NewService instead.
115+
// To provide a custom HTTP client, use option.WithHTTPClient.
116+
// If you are using google.golang.org/api/googleapis/transport.APIKey, use option.WithAPIKey with NewService instead.
117+
func New(client *http.Client) (*Service, error) {
118+
if client == nil {
119+
return nil, errors.New("client is nil")
120+
}
121+
s := &Service{client: client, BasePath: basePath}
122+
s.Atlas = NewAtlasService(s)
123+
return s, nil
124+
}
125+
126+
type Service struct {
127+
client *http.Client
128+
BasePath string // API endpoint base URL
129+
UserAgent string // optional additional User-Agent fragment
130+
131+
Atlas *AtlasService
132+
}
133+
134+
func (s *Service) userAgent() string {
135+
if s.UserAgent == "" {
136+
return googleapi.UserAgent
137+
}
138+
return googleapi.UserAgent + " " + s.UserAgent
139+
}
140+
141+
func NewAtlasService(s *Service) *AtlasService {
142+
rs := &AtlasService{s: s}
143+
return rs
144+
}
145+
146+
type AtlasService struct {
147+
s *Service
148+
}
149+
150+
type AtlasGetMapCall struct {
151+
s *Service
152+
req map[string]any
153+
urlParams_ gensupport.URLParams
154+
ifNoneMatch_ string
155+
ctx_ context.Context
156+
header_ http.Header
157+
}
158+
159+
// GetMap: Get a map.
160+
func (r *AtlasService) GetMap(req map[string]any) *AtlasGetMapCall {
161+
c := &AtlasGetMapCall{s: r.s, urlParams_: make(gensupport.URLParams)}
162+
c.req = req
163+
return c
164+
}
165+
166+
// Fields allows partial responses to be retrieved. See
167+
// https://developers.google.com/gdata/docs/2.0/basics#PartialResponse for more
168+
// details.
169+
func (c *AtlasGetMapCall) Fields(s ...googleapi.Field) *AtlasGetMapCall {
170+
c.urlParams_.Set("fields", googleapi.CombineFields(s))
171+
return c
172+
}
173+
174+
// IfNoneMatch sets an optional parameter which makes the operation fail if the
175+
// object's ETag matches the given value. This is useful for getting updates
176+
// only after the object has changed since the last request.
177+
func (c *AtlasGetMapCall) IfNoneMatch(entityTag string) *AtlasGetMapCall {
178+
c.ifNoneMatch_ = entityTag
179+
return c
180+
}
181+
182+
// Context sets the context to be used in this call's Do method.
183+
func (c *AtlasGetMapCall) Context(ctx context.Context) *AtlasGetMapCall {
184+
c.ctx_ = ctx
185+
return c
186+
}
187+
188+
// Header returns a http.Header that can be modified by the caller to add
189+
// headers to the request.
190+
func (c *AtlasGetMapCall) Header() http.Header {
191+
if c.header_ == nil {
192+
c.header_ = make(http.Header)
193+
}
194+
return c.header_
195+
}
196+
197+
func (c *AtlasGetMapCall) doRequest(alt string) (*http.Response, error) {
198+
reqHeaders := gensupport.SetHeaders(c.s.userAgent(), "", c.header_)
199+
if c.ifNoneMatch_ != "" {
200+
reqHeaders.Set("If-None-Match", c.ifNoneMatch_)
201+
}
202+
var body io.Reader = nil
203+
protoBytes, err := json.Marshal(c.req)
204+
if err != nil {
205+
return nil, err
206+
}
207+
body = bytes.NewReader(protoBytes)
208+
urls := googleapi.ResolveRelative(c.s.BasePath, "map")
209+
urls += "?" + c.urlParams_.Encode()
210+
req, err := http.NewRequest("GET", urls, body)
211+
if err != nil {
212+
return nil, err
213+
}
214+
req.Header = reqHeaders
215+
return gensupport.SendRequest(c.ctx_, c.s.client, req)
216+
}
217+
218+
// Do executes the "mapprotostruct.getMap" call.
219+
func (c *AtlasGetMapCall) Do(opts ...googleapi.CallOption) (map[string]any, error) {
220+
gensupport.SetOptions(c.urlParams_, opts...)
221+
res, err := c.doRequest("json")
222+
if err != nil {
223+
return nil, err
224+
}
225+
defer googleapi.CloseBody(res)
226+
if err := googleapi.CheckResponse(res); err != nil {
227+
return nil, gensupport.WrapError(err)
228+
}
229+
var ret map[string]any
230+
target := &ret
231+
if err := gensupport.DecodeResponse(target, res); err != nil {
232+
return nil, err
233+
}
234+
return ret, nil
235+
}

0 commit comments

Comments
 (0)