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 RequestOptions #4166

Merged
merged 6 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions generators/csharp/codegen/src/asIs/RawClient.Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ public async Task<ApiResponse> MakeRequestAsync(BaseApiRequest request)
httpRequest.Content = new StreamContent(streamRequest.Body);
}
// Send the request
var response = await Options.HttpClient.SendAsync(httpRequest);
var httpClient = request.Options?.HttpClient ?? Options.HttpClient;
var response = await httpClient.SendAsync(httpRequest);
return new ApiResponse { StatusCode = (int)response.StatusCode, Raw = response };
}

Expand All @@ -82,7 +83,7 @@ public record BaseApiRequest

public Dictionary<string, string> Headers { get; init; } = new();

public object? RequestOptions { get; init; }
public RequestOptions? Options { get; init; }
}

/// <summary>
Expand Down
15 changes: 15 additions & 0 deletions generators/csharp/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ 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).

## [0.6.0 - 2024-07-31]

- Feature: Add support for `RequestOptions`. Users can now specify a variety of request-specific
option overrides like the following:

```csharp
var user = client.GetUserAsync(
new GetUserRequest {
Username = "john.doe"
},
new RequestOptions {
BaseUrl = "https://localhost:3000"
}).Result;
```

## [0.5.0 - 2024-07-30]

- Feature: Add support for `uint`, `ulong`, and `float` types.
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.5.0
0.6.0
11 changes: 9 additions & 2 deletions generators/csharp/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { generateModels, generateTests } from "@fern-api/fern-csharp-model";
import { GeneratorNotificationService } from "@fern-api/generator-commons";
import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk";
import { HttpService, IntermediateRepresentation } from "@fern-fern/ir-sdk/api";
import { ClientOptionsGenerator } from "./client-options/ClientOptionsGenerator";
import { MultiUrlEnvironmentGenerator } from "./environment/MultiUrlEnvironmentGenerator";
import { SingleUrlEnvironmentGenerator } from "./environment/SingleUrlEnvironmentGenerator copy";
import { BaseOptionsGenerator } from "./options/BaseOptionsGenerator";
import { ClientOptionsGenerator } from "./options/ClientOptionsGenerator";
import { RequestOptionsGenerator } from "./options/RequestOptionsGenerator";
import { RootClientGenerator } from "./root-client/RootClientGenerator";
import { SdkCustomConfigSchema } from "./SdkCustomConfig";
import { SdkGeneratorContext } from "./SdkGeneratorContext";
Expand Down Expand Up @@ -83,9 +85,14 @@ export class SdkGeneratorCLI extends AbstractCsharpGeneratorCli<SdkCustomConfigS
}
}

const clientOptions = new ClientOptionsGenerator(context);
const baseOptionsGenerator = new BaseOptionsGenerator(context);

const clientOptions = new ClientOptionsGenerator(context, baseOptionsGenerator);
context.project.addSourceFiles(clientOptions.generate());

const requestOptions = new RequestOptionsGenerator(context, baseOptionsGenerator);
context.project.addSourceFiles(requestOptions.generate());

const rootClient = new RootClientGenerator(context);
context.project.addSourceFiles(rootClient.generate());

Expand Down
14 changes: 13 additions & 1 deletion generators/csharp/sdk/src/SdkGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
TypeReference
} from "@fern-fern/ir-sdk/api";
import { camelCase, upperFirst } from "lodash-es";
import { CLIENT_OPTIONS_CLASS_NAME } from "./client-options/ClientOptionsGenerator";
import { CLIENT_OPTIONS_CLASS_NAME } from "./options/ClientOptionsGenerator";
import { REQUEST_OPTIONS_CLASS_NAME, REQUEST_OPTIONS_PARAMETER_NAME } from "./options/RequestOptionsGenerator";
import { SdkCustomConfigSchema } from "./SdkCustomConfig";

const TYPES_FOLDER_NAME = "Types";
Expand Down Expand Up @@ -138,6 +139,17 @@ export class SdkGeneratorContext extends AbstractCsharpGeneratorContext<SdkCusto
});
}

public getRequestOptionsClassReference(): csharp.ClassReference {
return csharp.classReference({
name: REQUEST_OPTIONS_CLASS_NAME,
namespace: this.getCoreNamespace()
});
}

public getRequestOptionsParameterName(): string {
return REQUEST_OPTIONS_PARAMETER_NAME;
}

public getRequestWrapperReference(serviceId: ServiceId, requestName: Name): csharp.ClassReference {
const service = this.getHttpServiceOrThrow(serviceId);
RelativeFilePath.of([...service.name.fernFilepath.allParts.map((path) => path.pascalCase.safeName)].join("/"));
Expand Down
17 changes: 14 additions & 3 deletions generators/csharp/sdk/src/endpoint/EndpointGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { csharp } from "@fern-api/csharp-codegen";
import { HttpEndpoint, ServiceId } from "@fern-fern/ir-sdk/api";
import { BASE_URL_FIELD_NAME, ENVIRONMENT_FIELD_NAME } from "../options/BaseOptionsGenerator";
import { SdkGeneratorContext } from "../SdkGeneratorContext";
import { RawClient } from "./RawClient";
import { EndpointRequest } from "./request/EndpointRequest";
Expand All @@ -16,7 +17,6 @@ export declare namespace EndpointGenerator {
}
}

const REQUEST_PARAMETER_NAME = "request";
const RESPONSE_VARIABLE_NAME = "response";
const RESPONSE_BODY_VARIABLE_NAME = "responseBody";

Expand Down Expand Up @@ -52,6 +52,12 @@ export class EndpointGenerator {
})
);
}
parameters.push(
csharp.parameter({
type: csharp.Type.optional(csharp.Type.reference(this.context.getRequestOptionsClassReference())),
name: this.context.getRequestOptionsParameterName()
})
);
const return_ = this.getEndpointReturnType({ endpoint });
return csharp.method({
name: this.context.getEndpointMethodName(endpoint),
Expand Down Expand Up @@ -99,10 +105,15 @@ export class EndpointGenerator {
(baseUrlWithId) => baseUrlWithId.id === endpoint.baseUrl
);
if (baseUrl != null) {
return csharp.codeblock(`_client.Options.Environment.${baseUrl.name.pascalCase.safeName}`);
const requestOptionsEnvironment = `${this.context.getRequestOptionsParameterName()}?.${ENVIRONMENT_FIELD_NAME}`;
const environmentName = baseUrl.name.pascalCase.safeName;
return csharp.codeblock(
`${requestOptionsEnvironment}.${environmentName} ?? _client.Options.Environment.${environmentName}`
);
}
}
return csharp.codeblock("_client.Options.BaseUrl");
const requestOptionsBaseUrl = `${this.context.getRequestOptionsParameterName()}?.${BASE_URL_FIELD_NAME}`;
return csharp.codeblock(`${requestOptionsBaseUrl} ?? _client.Options.BaseUrl`);
}

private getEndpointRequest({
Expand Down
4 changes: 4 additions & 0 deletions generators/csharp/sdk/src/endpoint/RawClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export class RawClient {
assignment: csharp.codeblock(headerBagReference)
});
}
arguments_.push({
name: "Options",
assignment: csharp.codeblock(this.context.getRequestOptionsParameterName())
});
let apiRequest = csharp.instantiateClass({
arguments_,
classReference: csharp.classReference({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,72 +1,72 @@
import { csharp, CSharpFile, FileGenerator } from "@fern-api/csharp-codegen";
import { join, RelativeFilePath } from "@fern-api/fs-utils";
import { csharp } from "@fern-api/csharp-codegen";
import { Name } from "@fern-fern/ir-sdk/api";
import { SdkCustomConfigSchema } from "../SdkCustomConfig";
import { SdkGeneratorContext } from "../SdkGeneratorContext";

export const HTTP_CLIENT_FIELD = csharp.field({
access: "public",
name: "HttpClient",
get: true,
init: true,
type: csharp.Type.reference(
csharp.classReference({
name: "HttpClient",
namespace: "System.Net.Http"
})
),
initializer: csharp.codeblock("new HttpClient()"),
summary: "The http client used to make requests."
});
export const BASE_URL_FIELD_NAME = "BaseUrl";
export const ENVIRONMENT_FIELD_NAME = "Environment";

export const MAX_RETRIES_FIELD = csharp.field({
access: "public",
name: "MaxRetries",
get: true,
init: true,
type: csharp.Type.integer(),
initializer: csharp.codeblock("2"),
summary: "The http client used to make requests."
});
export interface OptionArgs {
optional: boolean;
includeInitializer: boolean;
}

export const TIMEOUT_IN_SECONDS = csharp.field({
access: "public",
name: "TimeoutInSeconds",
get: true,
init: true,
type: csharp.Type.integer(),
initializer: csharp.codeblock("30"),
summary: "The timeout for the request in seconds."
});
export class BaseOptionsGenerator {
private context: SdkGeneratorContext;

export const CLIENT_OPTIONS_CLASS_NAME = "ClientOptions";
constructor(context: SdkGeneratorContext) {
this.context = context;
}

export class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomConfigSchema, SdkGeneratorContext> {
public doGenerate(): CSharpFile {
const class_ = csharp.class_({
name: CLIENT_OPTIONS_CLASS_NAME,
namespace: this.context.getCoreNamespace(),
partial: true,
access: "public"
public getHttpClientField({ optional, includeInitializer }: OptionArgs): csharp.Field {
const type = csharp.Type.reference(
csharp.classReference({
name: "HttpClient",
namespace: "System.Net.Http"
})
);
return csharp.field({
access: "public",
name: "HttpClient",
get: true,
init: true,
type: optional ? csharp.Type.optional(type) : type,
initializer: includeInitializer ? csharp.codeblock("new HttpClient()") : undefined,
summary: "The http client used to make requests."
});
class_.addField(this.getBaseUrlField());
class_.addField(HTTP_CLIENT_FIELD);
class_.addField(MAX_RETRIES_FIELD);
class_.addField(TIMEOUT_IN_SECONDS);
return new CSharpFile({
clazz: class_,
directory: this.context.getCoreDirectory()
}

public getMaxRetriesField({ optional, includeInitializer }: OptionArgs): csharp.Field {
const type = csharp.Type.integer();
return csharp.field({
access: "public",
name: "MaxRetries",
get: true,
init: true,
type: optional ? csharp.Type.optional(type) : type,
initializer: includeInitializer ? csharp.codeblock("2") : undefined,
summary: "The http client used to make requests."
});
}

protected getFilepath(): RelativeFilePath {
return join(
this.context.project.filepaths.getCoreFilesDirectory(),
RelativeFilePath.of(`${CLIENT_OPTIONS_CLASS_NAME}.cs`)
public getTimeoutField({ optional, includeInitializer }: OptionArgs): csharp.Field {
const type = csharp.Types.reference(
csharp.classReference({
name: "TimeSpan",
namespace: "System"
})
);
return csharp.field({
access: "public",
name: "Timeout",
get: true,
init: true,
type: optional ? csharp.Type.optional(type) : type,
initializer: includeInitializer ? csharp.codeblock("new TimeSpan(0, 0, 30)") : undefined,
summary: "The timeout for the request."
});
}

private getBaseUrlField(): csharp.Field {
public getBaseUrlField({ optional, includeInitializer }: OptionArgs): csharp.Field {
const defaultEnvironmentId = this.context.ir.environments?.defaultEnvironment;
let defaultEnvironment: Name | undefined = undefined;
if (defaultEnvironmentId != null) {
Expand All @@ -88,39 +88,43 @@ export class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomC
if (this.context.ir.environments != null) {
const field = this.context.ir.environments.environments._visit({
singleBaseUrl: () => {
const type = csharp.Type.string();
return csharp.field({
access: "public",
name: "BaseUrl",
get: true,
init: true,
useRequired: defaultEnvironment != null,
type: csharp.Type.string(),
type: optional ? csharp.Type.optional(type) : type,
summary: "The Base URL for the API.",
initializer:
defaultEnvironment != null
initializer: includeInitializer
? defaultEnvironment != null
? csharp.codeblock((writer) => {
writer.writeNode(this.context.getEnvironmentsClassReference());
writer.write(`.${defaultEnvironment?.screamingSnakeCase.safeName}`);
})
: csharp.codeblock('""') // TODO: remove this logic since it sets url to ""
: undefined
});
},
multipleBaseUrls: () => {
const type = csharp.Type.reference(this.context.getEnvironmentsClassReference());
return csharp.field({
access: "public",
name: "Environment",
get: true,
init: true,
useRequired: defaultEnvironment != null,
type: csharp.Type.reference(this.context.getEnvironmentsClassReference()),
type: optional ? csharp.Type.optional(type) : type,
summary: "The Environment for the API.",
initializer:
defaultEnvironment != null
initializer: includeInitializer
? defaultEnvironment != null
? csharp.codeblock((writer) => {
writer.writeNode(this.context.getEnvironmentsClassReference());
writer.write(`.${defaultEnvironment?.screamingSnakeCase.safeName}`);
})
: csharp.codeblock("null") // TODO: remove this logic since it sets url to null
: undefined
});
},
_other: () => undefined
Expand All @@ -130,21 +134,23 @@ export class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomC
}
}

const type = csharp.Type.string();
return csharp.field({
access: "public",
name: "BaseUrl",
get: true,
init: true,
useRequired: defaultEnvironment != null,
type: csharp.Type.string(),
type: optional ? csharp.Type.optional(csharp.Type.string()) : type,
summary: "The Base URL for the API.",
initializer:
defaultEnvironment != null
initializer: includeInitializer
? defaultEnvironment != null
? csharp.codeblock((writer) => {
writer.writeNode(this.context.getEnvironmentsClassReference());
writer.write(`.${defaultEnvironment?.screamingSnakeCase.safeName}`);
})
: csharp.codeblock('""') // TODO: remove this logic since it sets url to ""
: undefined
});
}
}
Loading
Loading