Skip to content

Commit 58aaf86

Browse files
authored
fix(codegen): implement type-driven form encoding across SDKs (#1674)
Transition from explicitly assigning Content-Type headers inside generated methods to using native language Wrapper Types (e.g. URLSearchParams, FormValues) that the Runtime Library automatically deserializes for application/x-www-form-urlencoded behaviors. Fixes #1673
1 parent a3436b8 commit 58aaf86

19 files changed

Lines changed: 301 additions & 63 deletions

File tree

csharp/rtl/Constants.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,11 @@ public interface IValues : IDictionary<string, object>
8080
public class Values : Dictionary<string, object>, IValues
8181
{
8282
}
83+
84+
/// <summary>
85+
/// Type for explicitly specifying url-encoded form values
86+
/// </summary>
87+
public class FormValues : Values
88+
{
89+
}
8390
}

csharp/rtl/Transport.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,18 @@ public async Task<IRawResponse> RawRequest(
234234
Encoding.UTF8,
235235
"application/x-www-form-urlencoded");
236236
}
237+
else if (body is FormValues)
238+
{
239+
var dict = ((FormValues)body)
240+
.Where(k => k.Value != null)
241+
.ToDictionary(k => k.Key, k =>
242+
{
243+
if (k.Value is DateTime dt) return dt.ToString("o");
244+
if (k.Value is string || k.Value.GetType().IsPrimitive) return k.Value.ToString();
245+
return JsonSerializer.Serialize(k.Value, new JsonSerializerOptions { IgnoreNullValues = true });
246+
});
247+
request.Content = new FormUrlEncodedContent(dict);
248+
}
237249
else
238250
{
239251
request.Content =

go/rtl/auth.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ func NewAuthSessionWithTransport(config ApiSettings, transport http.RoundTripper
8787
}
8888
}
8989

90+
// Values is a map alias used for form urlencoded bodies
91+
type Values map[string]interface{}
92+
9093
func (s *AuthSession) Do(result interface{}, method, ver, path string, reqPars map[string]interface{}, body interface{}, options *ApiSettings) error {
9194

9295
// prepare URL
@@ -97,29 +100,36 @@ func (s *AuthSession) Do(result interface{}, method, ver, path string, reqPars m
97100

98101
// serialize body to string and determine request's Content-Type header
99102
if body != nil {
100-
// get the `body`'s type
101-
kind := reflect.TypeOf(body).Kind()
102-
value := reflect.ValueOf(body)
103-
104-
// check if it is pointer
105-
if kind == reflect.Ptr {
106-
// if so find the type it points to
107-
kind = reflect.ValueOf(body).Elem().Kind()
108-
value = reflect.ValueOf(body).Elem()
109-
}
110-
111-
if kind == reflect.String {
112-
contentTypeHeader = "text/plain"
113-
bodyString = fmt.Sprintf("%v", value)
103+
if v, ok := body.(Values); ok {
104+
contentTypeHeader = "application/x-www-form-urlencoded"
105+
u := &url.URL{}
106+
setQuery(u, map[string]interface{}(v))
107+
bodyString = u.RawQuery
114108
} else {
115-
contentTypeHeader = "application/json"
116-
serializedBody, err := json.Marshal(body)
117-
118-
if err != nil {
119-
_, _ = fmt.Fprintf(os.Stderr, "error serializing body: %v", err)
109+
// get the `body`'s type
110+
kind := reflect.TypeOf(body).Kind()
111+
value := reflect.ValueOf(body)
112+
113+
// check if it is pointer
114+
if kind == reflect.Ptr {
115+
// if so find the type it points to
116+
kind = reflect.ValueOf(body).Elem().Kind()
117+
value = reflect.ValueOf(body).Elem()
120118
}
121119

122-
bodyString = string(serializedBody)
120+
if kind == reflect.String {
121+
contentTypeHeader = "text/plain"
122+
bodyString = fmt.Sprintf("%v", value)
123+
} else {
124+
contentTypeHeader = "application/json"
125+
serializedBody, err := json.Marshal(body)
126+
127+
if err != nil {
128+
_, _ = fmt.Fprintf(os.Stderr, "error serializing body: %v", err)
129+
}
130+
131+
bodyString = string(serializedBody)
132+
}
123133
}
124134
}
125135

kotlin/src/main/com/looker/rtl/Transport.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,10 @@ fun encodeParam(value: Any?): String {
217217
value.toOffsetDateTime().format(utcFormat)
218218
} else if (value is Date) {
219219
value.toInstant().atZone(ZoneOffset.UTC).format(utcFormat)
220-
} else {
220+
} else if (value is String || value is Number || value is Boolean) {
221221
"$value"
222+
} else {
223+
GSON.toJson(value)
222224
}
223225
try {
224226
val decoded = URLDecoder.decode(encoded, utf8)

packages/sdk-codegen/src/codeGen.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,12 +1317,13 @@ export abstract class CodeGen implements ICodeGen {
13171317
assignFormArgs(
13181318
method: IMethod,
13191319
body: string,
1320-
query: string
1321-
): { body: string; query: string } {
1320+
query: string,
1321+
options: string
1322+
): { body: string; query: string; options: string } {
13221323
if (method.isFormUrlEncoded) {
1323-
return { body: query, query: this.nullStr };
1324+
return { body: query, query: this.nullStr, options };
13241325
}
1325-
return { body, query };
1326+
return { body, query, options };
13261327
}
13271328

13281329
// build the http argument list from back to front, so trailing undefined arguments
@@ -1338,9 +1339,13 @@ export abstract class CodeGen implements ICodeGen {
13381339
result = this.argFill(result, this.argGroup(indent, method.headerArgs));
13391340
let body = method.bodyArg ? method.bodyArg : this.nullStr;
13401341
let query = this.argGroup(indent, method.queryArgs);
1341-
const formArgs = this.assignFormArgs(method, body, query);
1342+
// Add 'options' since it might be modified for form encoding
1343+
let options = this.nullStr;
1344+
const formArgs = this.assignFormArgs(method, body, query, options);
13421345
body = formArgs.body;
13431346
query = formArgs.query;
1347+
options = formArgs.options;
1348+
result = this.argFill(result, options);
13441349
result = this.argFill(result, body);
13451350
result = this.argFill(result, query);
13461351
return result;

packages/sdk-codegen/src/csharp.gen.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,15 +341,30 @@ namespace Looker.SDK.API${this.apiRef}
341341
: this.nullStr;
342342
}
343343

344+
assignFormArgs(
345+
method: IMethod,
346+
body: string,
347+
query: string,
348+
options: string
349+
): { body: string; query: string; options: string } {
350+
if (method.isFormUrlEncoded) {
351+
// CSharp uses "new FormValues { ... }" to trigger RTL-level URL encoding.
352+
query = query.replace(/new Values \{/g, 'new FormValues {');
353+
return { body: query, query: this.nullStr, options };
354+
}
355+
return { body, query, options };
356+
}
357+
344358
httpArgs(indent: string, method: IMethod) {
345-
let result = this.argFill('', 'options');
346-
// result = this.argFill(result, this.argGroup(indent, method.cookieArgs))
347-
// result = this.argFill(result, this.argGroup(indent, method.headerArgs))
359+
let options = 'options';
348360
let body = method.bodyArg ? method.bodyArg : this.nullStr;
349361
let query = this.argGroup(indent, method.queryArgs);
350-
const formArgs = this.assignFormArgs(method, body, query);
362+
const formArgs = this.assignFormArgs(method, body, query, options);
351363
body = formArgs.body;
352364
query = formArgs.query;
365+
options = formArgs.options;
366+
367+
let result = this.argFill('', options);
353368
result = this.argFill(result, body);
354369
result = this.argFill(result, query);
355370
return result;

packages/sdk-codegen/src/go.gen.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,21 +284,35 @@ export class GoGen extends CodeGen {
284284
// add options at the end of the request calls. this will cause all other arguments to be
285285
// filled in but there's no way to avoid this for passing in the last optional parameter.
286286
// Fortunately, this code bloat is minimal and also hidden from the consumer.
287-
let result = this.argFill('', 'options');
288-
// let result = this.argFill('', this.argGroup(indent, method.cookieArgs, request))
289-
// result = this.argFill(result, this.argGroup(indent, method.headerArgs, request))
287+
let options = 'options';
290288
let body = method.bodyArg
291289
? this.accessor(method.bodyArg, request)
292290
: this.nullStr;
293291
let query = this.argGroup(indent, method.queryArgs, request);
294-
const formArgs = this.assignFormArgs(method, body, query);
292+
const formArgs = this.assignFormArgs(method, body, query, options);
295293
body = formArgs.body;
296294
query = formArgs.query;
295+
options = formArgs.options;
296+
297+
let result = this.argFill('', options);
297298
result = this.argFill(result, body);
298299
result = this.argFill(result, query);
299300
return result;
300301
}
301302

303+
assignFormArgs(
304+
method: IMethod,
305+
body: string,
306+
query: string,
307+
options: string
308+
): { body: string; query: string; options: string } {
309+
if (method.isFormUrlEncoded) {
310+
body = query.replace('map[string]interface{}', 'rtl.Values');
311+
query = this.nullStr;
312+
}
313+
return { body, query, options };
314+
}
315+
302316
argGroup(_indent: string, args: Arg[], prefix?: string) {
303317
if (!args || args.length === 0) return this.nullStr;
304318
const hash: string[] = [];

packages/sdk-codegen/src/kotlin.gen.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,12 +458,16 @@ ${props.join(this.propDelimiter)}
458458
assignFormArgs(
459459
method: IMethod,
460460
body: string,
461-
query: string
462-
): { body: string; query: string } {
463-
const result = super.assignFormArgs(method, body, query);
461+
query: string,
462+
options: string
463+
): { body: string; query: string; options: string } {
464+
const result = super.assignFormArgs(method, body, query, options);
464465
if (result.query === this.nullStr) {
465466
result.query = 'mapOf()';
466467
}
468+
if (method.isFormUrlEncoded) {
469+
result.body = `encodeValues(${result.body})`;
470+
}
467471
return result;
468472
}
469473

@@ -483,11 +487,13 @@ ${props.join(this.propDelimiter)}
483487
// let result = this.argFill('', 'options')
484488
// let result = this.argFill('', this.argGroup(indent, method.cookieArgs, request))
485489
// result = this.argFill(result, this.argGroup(indent, method.headerArgs, request))
490+
let options = 'options';
486491
let body = method.bodyArg ? `${request}${method.bodyArg}` : this.nullStr;
487492
let query = this.argGroup(indent, method.queryArgs, request);
488-
const formArgs = this.assignFormArgs(method, body, query);
493+
const formArgs = this.assignFormArgs(method, body, query, options);
489494
body = formArgs.body;
490495
query = formArgs.query;
496+
options = formArgs.options; // Kotlin generator ignores options down the line currently
491497
let result = this.argFill('', body);
492498
result = this.argFill(result, query);
493499
return result;

packages/sdk-codegen/src/python.gen.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import warnings
8181
8282
from . import models as mdls
8383
from looker_sdk.rtl import api_methods
84+
from looker_sdk.rtl import model
8485
from looker_sdk.rtl import transport
8586
8687
class LookerSDK(api_methods.APIMethods):

packages/sdk-codegen/src/python.gen.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import warnings
105105
106106
from . import models as mdls
107107
from ${this.packagePath}.rtl import api_methods
108+
from ${this.packagePath}.rtl import model
108109
from ${this.packagePath}.rtl import transport
109110
110111
class ${this.packageName}(api_methods.APIMethods):
@@ -371,16 +372,22 @@ ${this.hooks.join('\n')}
371372

372373
httpArgs(callerIndent: string, method: IMethod): string {
373374
const currIndent = this.bumper(callerIndent);
374-
let args = '';
375-
args = this.argFill(
376-
args,
377-
`${currIndent}transport_options=transport_options`
378-
);
379375
const body = method.bodyArg ? method.bodyArg : this.nullStr;
380376
const query = method.queryArgs.length
381377
? this.argGroup('', method.queryArgs)
382378
: this.nullStr;
383-
const formArgs = this.assignFormArgs(method, body, query);
379+
const formArgs = this.assignFormArgs(
380+
method,
381+
body,
382+
query,
383+
'transport_options'
384+
);
385+
386+
let args = '';
387+
args = this.argFill(
388+
args,
389+
`${currIndent}transport_options=${formArgs.options}`
390+
);
384391

385392
if (formArgs.body !== this.nullStr) {
386393
args = this.argFill(args, `${currIndent}body=${formArgs.body}`);
@@ -438,6 +445,19 @@ ${this.hooks.join('\n')}
438445
return encodings;
439446
}
440447

448+
assignFormArgs(
449+
method: IMethod,
450+
body: string,
451+
query: string,
452+
options: string
453+
): { body: string; query: string; options: string } {
454+
if (method.isFormUrlEncoded) {
455+
body = `model.URLSearchParams(${query})`;
456+
query = this.nullStr;
457+
}
458+
return { body, query, options };
459+
}
460+
441461
declareMethod(indent: string, method: IMethod) {
442462
const bump = this.bumper(indent);
443463

0 commit comments

Comments
 (0)