Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feature, csharp): Add support for allow-multiple query params #4157

Merged
merged 10 commits into from
Jul 31, 2024
19 changes: 18 additions & 1 deletion generators/csharp/codegen/src/asIs/RawClient.Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,24 @@ private string BuildUrl(BaseApiRequest request)
url += "?";
url = request.Query.Aggregate(
url,
(current, queryItem) => current + $"{queryItem.Key}={queryItem.Value}&"
(current, queryItem) =>
{
if (queryItem.Value is System.Collections.IEnumerable collection and not string)
{
var items = collection
.Cast<object>()
.Select(value => $"{queryItem.Key}={value}")
.ToList();
if (items.Any()) {
current += string.Join("&", items) + "&";
}
}
else
{
current += $"{queryItem.Key}={queryItem.Value}&";
}
return current;
}
);
url = url.Substring(0, url.Length - 1);
return url;
Expand Down
1 change: 1 addition & 0 deletions generators/csharp/codegen/src/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { Class } from "./Class";
export { ClassInstantiation } from "./ClassInstantiation";
export { ClassReference } from "./ClassReference";
export { CodeBlock } from "./CodeBlock";
export { Writer } from "./core/Writer";
export { CoreClassReference } from "./CoreClassReference";
export * as dependencies from "./dependencies";
export { Dictionary } from "./Dictionary";
Expand Down
3 changes: 2 additions & 1 deletion generators/csharp/codegen/src/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export {
MethodInvocation,
MethodType,
Parameter,
Type
Type,
Writer
} from "./ast";
export { AstNode } from "./ast/core/AstNode";
12 changes: 11 additions & 1 deletion generators/csharp/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
## [0.4.0 - 2024-07-30]

- Feature: Add support for `allow-multiple` query parameters, which are sent in the `explode` format.
Given that optional lists are assigned a default, empty list, we use a simple `LINQ` expression to
handle the serialization, which is shown below:

```csharp
_query["operand"] = request
.Operand.Select(_value => JsonSerializer.Serialize(_value))
.ToList();
```

- Improvement: `map<string, unknown>` types are now generated as `Dictionary<string, object?>` types so they
can support explicit `null` values. Note that this does _not_ affect every `unknown` type to be an `object?`
Expand Down
2 changes: 1 addition & 1 deletion generators/csharp/sdk/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.4
0.4.0
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class WrappedEndpointRequest extends EndpointRequest {
const requiredQueryParameters: QueryParameter[] = [];
const optionalQueryParameters: QueryParameter[] = [];
for (const queryParameter of this.endpoint.queryParameters) {
if (this.context.isOptional(queryParameter.valueType)) {
if (!queryParameter.allowMultiple && this.context.isOptional(queryParameter.valueType)) {
optionalQueryParameters.push(queryParameter);
} else {
requiredQueryParameters.push(queryParameter);
Expand All @@ -66,29 +66,46 @@ export class WrappedEndpointRequest extends EndpointRequest {
csharp.dictionary({
keyType: csharp.Type.string(),
valueType: csharp.Type.object(),
entries: requiredQueryParameters.map((queryParameter) => {
return {
key: csharp.codeblock(`"${queryParameter.name.wireValue}"`),
value: this.stringify({
reference: queryParameter.valueType,
name: queryParameter.name.name
})
};
})
entries: []
})
);
for (const query of requiredQueryParameters) {
this.writeQueryParameter(writer, query);
}
for (const query of optionalQueryParameters) {
const queryParameterReference = `${this.getParameterName()}.${query.name.name.pascalCase.safeName}`;
writer.controlFlow("if", `${queryParameterReference} != null`);
writer.write(`${QUERY_PARAMETER_BAG_NAME}["${query.name.wireValue}"] = `);
writer.writeNodeStatement(this.stringify({ reference: query.valueType, name: query.name.name }));
this.writeQueryParameter(writer, query);
writer.endControlFlow();
}
}),
queryParameterBagReference: QUERY_PARAMETER_BAG_NAME
};
}

private writeQueryParameter(writer: csharp.Writer, query: QueryParameter): void {
writer.write(`${QUERY_PARAMETER_BAG_NAME}["${query.name.wireValue}"] = `);
if (!query.allowMultiple) {
writer.writeNodeStatement(this.stringify({ reference: query.valueType, name: query.name.name }));
return;
}
const queryParameterReference = `${this.getParameterName()}.${query.name.name.pascalCase.safeName}`;
if (this.isString(query.valueType)) {
writer.writeLine(`${queryParameterReference};`);
return;
}
writer.write(`${queryParameterReference}.Select(_value => `);
writer.writeNode(
this.stringify({
reference: query.valueType,
name: query.name.name,
parameterOverride: "_value",
allowOptionals: false // When allow-multiple is set, the query parameter never uses optional types.
})
);
writer.writeLine(").ToList();");
}

public getHeaderParameterCodeBlock(): HeaderParameterCodeBlock | undefined {
if (this.endpoint.headers.length === 0) {
return undefined;
Expand Down Expand Up @@ -144,43 +161,54 @@ export class WrappedEndpointRequest extends EndpointRequest {
return undefined;
}

private stringify({ reference, name }: { reference: TypeReference; name: Name }): csharp.CodeBlock {
private stringify({
reference,
name,
parameterOverride,
allowOptionals
}: {
reference: TypeReference;
name: Name;
parameterOverride?: string;
allowOptionals?: boolean;
}): csharp.CodeBlock {
const parameter = parameterOverride ?? `${this.getParameterName()}.${name.pascalCase.safeName}`;
if (this.isString(reference)) {
return csharp.codeblock(`${this.getParameterName()}.${name.pascalCase.safeName}`);
} else if (this.isDatetime({ typeReference: reference, allowOptionals: false })) {
return csharp.codeblock(`${parameter}`);
} else if (this.isDatetime({ typeReference: reference, allowOptionals: allowOptionals ?? false })) {
return csharp.codeblock((writer) => {
writer.write(`${this.getParameterName()}.${name.pascalCase.safeName}.ToString(`);
writer.write(`${parameter}.ToString(`);
writer.writeNode(this.context.getConstantsClassReference());
writer.write(".DateTimeFormat)");
});
} else if (this.isDatetime({ typeReference: reference, allowOptionals: true })) {
} else if (this.isDatetime({ typeReference: reference, allowOptionals: allowOptionals ?? true })) {
return csharp.codeblock((writer) => {
writer.write(`${this.getParameterName()}.${name.pascalCase.safeName}.Value.ToString(`);
writer.write(`${parameter}.Value.ToString(`);
writer.writeNode(this.context.getConstantsClassReference());
writer.write(".DateTimeFormat)");
});
} else if (this.isEnum({ typeReference: reference, allowOptionals: false })) {
} else if (this.isEnum({ typeReference: reference, allowOptionals: allowOptionals ?? false })) {
return csharp.codeblock((writer) => {
writer.writeNode(
csharp.classReference({
name: "JsonSerializer",
namespace: "System.Text.Json"
})
);
writer.write(`.Serialize(${this.getParameterName()}.${name.pascalCase.safeName})`);
writer.write(`.Serialize(${parameter})`);
});
} else if (this.isEnum({ typeReference: reference, allowOptionals: true })) {
} else if (this.isEnum({ typeReference: reference, allowOptionals: allowOptionals ?? true })) {
return csharp.codeblock((writer) => {
writer.writeNode(
csharp.classReference({
name: "JsonSerializer",
namespace: "System.Text.Json"
})
);
writer.write(`.Serialize(${this.getParameterName()}.${name.pascalCase.safeName}.Value)`);
writer.write(`.Serialize(${parameter}.Value)`);
});
} else {
return csharp.codeblock(`${this.getParameterName()}.${name.pascalCase.safeName}.ToString()`);
return csharp.codeblock(`${parameter}.ToString()`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,15 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
});

for (const query of this.endpoint.queryParameters) {
const type = query.allowMultiple
? csharp.Type.list(
this.context.csharpTypeMapper.convert({ reference: query.valueType, unboxOptionals: true })
)
: this.context.csharpTypeMapper.convert({ reference: query.valueType });
class_.addField(
csharp.field({
name: query.name.name.pascalCase.safeName,
type: this.context.csharpTypeMapper.convert({ reference: query.valueType }),
type,
access: "public",
get: true,
set: true,
Expand Down
20 changes: 19 additions & 1 deletion seed/csharp-sdk/alias/src/SeedAlias/Core/RawClient.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion seed/csharp-sdk/audiences/src/SeedAudiences/Core/RawClient.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/RawClient.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading