Skip to content

Commit

Permalink
feat(csharp): Add support for inline-path-parameters (#6228)
Browse files Browse the repository at this point in the history
  • Loading branch information
amckinney authored Feb 28, 2025
1 parent b7fa7fb commit 6eff487
Show file tree
Hide file tree
Showing 176 changed files with 11,381 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,14 @@ export abstract class AbstractCsharpGeneratorContext<
});
}

public getJsonIgnoreAnnotation(): csharp.Annotation {
return csharp.annotation({
reference: csharp.classReference({
name: "JsonIgnore",
namespace: "System.Text.Json.Serialization"
})
});
}
public getPascalCaseSafeName(name: Name): string {
return name.pascalCase.safeName;
}
Expand Down
3 changes: 2 additions & 1 deletion generators/csharp/sdk/src/SdkCustomConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const SdkCustomConfigSchema = z.strictObject({
),
"read-only-memory-types": z.optional(z.array(z.string())),
"experimental-enable-forward-compatible-enums": z.boolean().optional(),
"generate-mock-server-tests": z.boolean().optional()
"generate-mock-server-tests": z.boolean().optional(),
"inline-path-parameters": z.boolean().optional()
});

export type SdkCustomConfigSchema = z.infer<typeof SdkCustomConfigSchema>;
16 changes: 16 additions & 0 deletions generators/csharp/sdk/src/SdkGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
NameAndWireValue,
OAuthScheme,
ProtobufService,
SdkRequestWrapper,
ServiceId,
Subpackage,
SubpackageId,
Expand Down Expand Up @@ -132,6 +133,21 @@ export class SdkGeneratorContext extends AbstractCsharpGeneratorContext<SdkCusto
return EndpointSnippetsGenerator.CLIENT_VARIABLE_NAME;
}

public includePathParametersInWrappedRequest({
endpoint,
wrapper
}: {
endpoint: HttpEndpoint;
wrapper: SdkRequestWrapper;
}): boolean {
const inlinePathParameters = this.customConfig["inline-path-parameters"];
if (inlinePathParameters == null) {
return false;
}
const wrapperShouldIncludePathParameters = wrapper.includePathParameters ?? false;
return endpoint.allPathParameters.length > 0 && inlinePathParameters && wrapperShouldIncludePathParameters;
}

public getRawAsIsFiles(): string[] {
return [AsIsFiles.GitIgnore];
}
Expand Down
82 changes: 63 additions & 19 deletions generators/csharp/sdk/src/endpoint/AbstractEndpointGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assertNever } from "@fern-api/core-utils";
import { csharp } from "@fern-api/csharp-codegen";
import { ExampleGenerator } from "@fern-api/fern-csharp-model";

import { ExampleEndpointCall, ExampleRequestBody, HttpEndpoint, ServiceId } from "@fern-fern/ir-sdk/api";
import { ExampleEndpointCall, ExampleRequestBody, HttpEndpoint, PathParameter, ServiceId } from "@fern-fern/ir-sdk/api";

import { SdkGeneratorContext } from "../SdkGeneratorContext";
import { WrappedRequestGenerator } from "../wrapped-request/WrappedRequestGenerator";
Expand Down Expand Up @@ -40,12 +40,16 @@ export abstract class AbstractEndpointGenerator {
serviceId: ServiceId;
endpoint: HttpEndpoint;
}): EndpointSignatureInfo {
const { pathParameters, pathParameterReferences } = this.getAllPathParameters({ serviceId, endpoint });
const request = getEndpointRequest({ context: this.context, endpoint, serviceId });
const requestParameter =
request != null
? csharp.parameter({ type: request.getParameterType(), name: request.getParameterName() })
: undefined;
const { pathParameters, pathParameterReferences } = this.getAllPathParameters({
serviceId,
endpoint,
requestParameter
});
return {
baseParameters: [...pathParameters, requestParameter].filter((p): p is csharp.Parameter => p != null),
pathParameters,
Expand Down Expand Up @@ -113,28 +117,32 @@ export abstract class AbstractEndpointGenerator {

protected getAllPathParameters({
serviceId,
endpoint
endpoint,
requestParameter
}: {
serviceId: ServiceId;
endpoint: HttpEndpoint;
requestParameter?: csharp.Parameter;
}): Pick<EndpointSignatureInfo, "pathParameters" | "pathParameterReferences"> {
const pathParameters: csharp.Parameter[] = [];
const service = this.context.getHttpServiceOrThrow(serviceId);
const pathParameterReferences: Record<string, string> = {};
for (const pathParam of [
...this.context.ir.pathParameters,
...service.pathParameters,
...endpoint.pathParameters
]) {
const parameterName = pathParam.name.camelCase.safeName;
const includePathParametersInEndpointSignature = this.includePathParametersInEndpointSignature({ endpoint });
for (const pathParam of endpoint.allPathParameters) {
const parameterName = this.getPathParameterName({
pathParameter: pathParam,
includePathParametersInEndpointSignature,
requestParameter
});
if (includePathParametersInEndpointSignature) {
pathParameters.push(
csharp.parameter({
docs: pathParam.docs,
name: parameterName,
type: this.context.csharpTypeMapper.convert({ reference: pathParam.valueType })
})
);
}
pathParameterReferences[pathParam.name.originalName] = parameterName;
pathParameters.push(
csharp.parameter({
docs: pathParam.docs,
name: parameterName,
type: this.context.csharpTypeMapper.convert({ reference: pathParam.valueType })
})
);
}
return {
pathParameters,
Expand Down Expand Up @@ -182,7 +190,7 @@ export abstract class AbstractEndpointGenerator {
if (requestBodyType === "fileUpload" || requestBodyType === "bytes") {
return undefined;
}
const args = this.getNonEndpointArguments(example, parseDatetimes);
const args = this.getNonEndpointArguments({ endpoint, example, parseDatetimes });
const endpointRequestSnippet = this.getEndpointRequestSnippet(example, endpoint, serviceId, parseDatetimes);
if (endpointRequestSnippet != null) {
args.push(endpointRequestSnippet);
Expand Down Expand Up @@ -245,7 +253,18 @@ export abstract class AbstractEndpointGenerator {
});
}

protected getNonEndpointArguments(example: ExampleEndpointCall, parseDatetimes: boolean): csharp.CodeBlock[] {
protected getNonEndpointArguments({
endpoint,
example,
parseDatetimes
}: {
endpoint: HttpEndpoint;
example: ExampleEndpointCall;
parseDatetimes: boolean;
}): csharp.CodeBlock[] {
if (!this.includePathParametersInEndpointSignature({ endpoint })) {
return [];
}
const pathParameters = [
...example.rootPathParameters,
...example.servicePathParameters,
Expand All @@ -258,4 +277,29 @@ export abstract class AbstractEndpointGenerator {
})
);
}

private includePathParametersInEndpointSignature({ endpoint }: { endpoint: HttpEndpoint }): boolean {
if (endpoint.sdkRequest?.shape.type !== "wrapper") {
return true;
}
return !this.context.includePathParametersInWrappedRequest({
endpoint,
wrapper: endpoint.sdkRequest.shape
});
}

private getPathParameterName({
pathParameter,
includePathParametersInEndpointSignature,
requestParameter
}: {
pathParameter: PathParameter;
includePathParametersInEndpointSignature: boolean;
requestParameter?: csharp.Parameter;
}): string {
if (!includePathParametersInEndpointSignature && requestParameter != null) {
return `${requestParameter?.name}.${pathParameter.name.pascalCase.safeName}`;
}
return pathParameter.name.camelCase.safeName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
}): csharp.MethodInvocation | undefined {
const service = this.context.getHttpServiceOrThrow(serviceId);
const serviceFilePath = service.name.fernFilepath;
const args = this.getNonEndpointArguments(example, parseDatetimes);
const args = this.getNonEndpointArguments({ endpoint, example, parseDatetimes });
const endpointRequestSnippet = this.getEndpointRequestSnippet(example, endpoint, serviceId, parseDatetimes);
if (endpointRequestSnippet != null) {
args.push(endpointRequestSnippet);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
const service = this.context.getHttpServiceOrThrow(this.serviceId);
const isProtoRequest = this.context.endpointUsesGrpcTransport(service, this.endpoint);
const protobufProperties: { propertyName: string; typeReference: TypeReference }[] = [];

if (this.context.includePathParametersInWrappedRequest({ endpoint: this.endpoint, wrapper: this.wrapper })) {
for (const pathParameter of this.endpoint.allPathParameters) {
class_.addField(
csharp.field({
name: pathParameter.name.pascalCase.safeName,
type: this.context.csharpTypeMapper.convert({ reference: pathParameter.valueType }),
access: csharp.Access.Public,
get: true,
set: true,
summary: pathParameter.docs,
useRequired: true,
initializer: this.context.getLiteralInitializerFromTypeReference({
typeReference: pathParameter.valueType
}),
annotations: [this.context.getJsonIgnoreAnnotation()]
})
);
}
}

for (const query of this.endpoint.queryParameters) {
const propertyName = query.name.name.pascalCase.safeName;
const type = query.allowMultiple
Expand All @@ -69,10 +90,10 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
useRequired: true,
initializer: this.context.getLiteralInitializerFromTypeReference({
typeReference: query.valueType
})
}),
annotations: [this.context.getJsonIgnoreAnnotation()]
})
);

if (isProtoRequest) {
protobufProperties.push({
propertyName,
Expand All @@ -94,7 +115,8 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
useRequired: true,
initializer: this.context.getLiteralInitializerFromTypeReference({
typeReference: header.valueType
})
}),
annotations: [this.context.getJsonIgnoreAnnotation()]
})
);
}
Expand Down Expand Up @@ -186,6 +208,21 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
parseDatetimes: boolean;
}): csharp.CodeBlock {
const orderedFields: { name: Name; value: csharp.CodeBlock }[] = [];
if (this.context.includePathParametersInWrappedRequest({ endpoint: this.endpoint, wrapper: this.wrapper })) {
for (const pathParameter of [
...example.rootPathParameters,
...example.servicePathParameters,
...example.endpointPathParameters
]) {
orderedFields.push({
name: pathParameter.name,
value: this.exampleGenerator.getSnippetForTypeReference({
exampleTypeReference: pathParameter.value,
parseDatetimes
})
});
}
}
for (const exampleQueryParameter of example.queryParameters) {
const isSingleQueryParameter =
exampleQueryParameter.shape == null || exampleQueryParameter.shape.type === "single";
Expand Down
22 changes: 22 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@
# Set `enable-forward-compatible-enums` to `false` in the configuration to generate the old enums.
# irVersion: 53

- version: 1.11.0
createdAt: "2025-02-27"
irVersion: 55
changelogEntry:
- type: feat
summary: |
Add support for the `inline-path-parameters` configuration option, which generates
path parameters in the generated request type (if any) instead of as separate
positional parameters.
```yaml
# generators.yml
- name: fern-api/fern-csharp-sdk
version: 1.11.0
config:
inline-path-parameters: true
```
- type: fix
summary: |
Fix an issue where the `JsonIgnore` attribute was not included for query parameter or header properties.
- version: 1.10.1
createdAt: "2025-02-27"
irVersion: 55
Expand Down

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.

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.

Loading

0 comments on commit 6eff487

Please sign in to comment.