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 all 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
8 changes: 5 additions & 3 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 Expand Up @@ -113,7 +114,8 @@ public record ApiResponse

private string BuildUrl(BaseApiRequest request)
{
var trimmedBaseUrl = request.BaseUrl.TrimEnd('/');
var baseUrl = request.Options?.BaseUrl ?? request.BaseUrl;
var trimmedBaseUrl = baseUrl.TrimEnd('/');
var trimmedBasePath = request.Path.TrimStart('/');
var url = $"{trimmedBaseUrl}/{trimmedBasePath}";
if (request.Query.Count <= 0)
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
8 changes: 7 additions & 1 deletion generators/csharp/sdk/src/endpoint/EndpointGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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 +51,13 @@ export class EndpointGenerator {
})
);
}
parameters.push(
csharp.parameter({
type: csharp.Type.optional(csharp.Type.reference(this.context.getRequestOptionsClassReference())),
name: this.context.getRequestOptionsParameterName(),
initializer: "null"
})
);
const return_ = this.getEndpointReturnType({ endpoint });
return csharp.method({
name: this.context.getEndpointMethodName(endpoint),
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
67 changes: 67 additions & 0 deletions generators/csharp/sdk/src/options/BaseOptionsGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { csharp } from "@fern-api/csharp-codegen";
import { SdkGeneratorContext } from "../SdkGeneratorContext";

export const BASE_URL_FIELD_NAME = "BaseUrl";
export const BASE_URL_SUMMARY = "The Base URL for the API.";

export interface OptionArgs {
optional: boolean;
includeInitializer: boolean;
}

export class BaseOptionsGenerator {
private context: SdkGeneratorContext;

constructor(context: SdkGeneratorContext) {
this.context = context;
}

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."
});
}

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."
});
}

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("TimeSpan.FromSeconds(30)") : undefined,
summary: "The timeout for the request."
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,34 @@ import { join, RelativeFilePath } from "@fern-api/fs-utils";
import { Name } from "@fern-fern/ir-sdk/api";
import { SdkCustomConfigSchema } from "../SdkCustomConfig";
import { SdkGeneratorContext } from "../SdkGeneratorContext";
import { BaseOptionsGenerator, BASE_URL_FIELD_NAME, BASE_URL_SUMMARY, OptionArgs } from "./BaseOptionsGenerator";

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 CLIENT_OPTIONS_CLASS_NAME = "ClientOptions";

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 class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomConfigSchema, SdkGeneratorContext> {
private baseOptionsGenerator: BaseOptionsGenerator;

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."
});
constructor(context: SdkGeneratorContext, baseOptionsGenerator: BaseOptionsGenerator) {
super(context);

export const CLIENT_OPTIONS_CLASS_NAME = "ClientOptions";
this.baseOptionsGenerator = baseOptionsGenerator;
}

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"
});
const optionArgs: OptionArgs = {
optional: false,
includeInitializer: true
};
class_.addField(this.getBaseUrlField());
class_.addField(HTTP_CLIENT_FIELD);
class_.addField(MAX_RETRIES_FIELD);
class_.addField(TIMEOUT_IN_SECONDS);
class_.addField(this.baseOptionsGenerator.getHttpClientField(optionArgs));
class_.addField(this.baseOptionsGenerator.getMaxRetriesField(optionArgs));
class_.addField(this.baseOptionsGenerator.getTimeoutField(optionArgs));
return new CSharpFile({
clazz: class_,
directory: this.context.getCoreDirectory()
Expand Down Expand Up @@ -90,12 +68,12 @@ export class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomC
singleBaseUrl: () => {
return csharp.field({
access: "public",
name: "BaseUrl",
name: BASE_URL_FIELD_NAME,
get: true,
init: true,
useRequired: defaultEnvironment != null,
type: csharp.Type.string(),
summary: "The Base URL for the API.",
summary: BASE_URL_SUMMARY,
initializer:
defaultEnvironment != null
? csharp.codeblock((writer) => {
Expand Down Expand Up @@ -132,12 +110,12 @@ export class ClientOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomC

return csharp.field({
access: "public",
name: "BaseUrl",
name: BASE_URL_FIELD_NAME,
get: true,
init: true,
useRequired: defaultEnvironment != null,
type: csharp.Type.string(),
summary: "The Base URL for the API.",
summary: BASE_URL_SUMMARY,
initializer:
defaultEnvironment != null
? csharp.codeblock((writer) => {
Expand Down
55 changes: 55 additions & 0 deletions generators/csharp/sdk/src/options/RequestOptionsGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { csharp, CSharpFile, FileGenerator } from "@fern-api/csharp-codegen";
import { join, RelativeFilePath } from "@fern-api/fs-utils";
import { SdkCustomConfigSchema } from "../SdkCustomConfig";
import { SdkGeneratorContext } from "../SdkGeneratorContext";
import { BaseOptionsGenerator, BASE_URL_FIELD_NAME, BASE_URL_SUMMARY, OptionArgs } from "./BaseOptionsGenerator";

export const REQUEST_OPTIONS_CLASS_NAME = "RequestOptions";
export const REQUEST_OPTIONS_PARAMETER_NAME = "options";

const BASE_URL_FIELD = csharp.field({
access: "public",
name: BASE_URL_FIELD_NAME,
get: true,
init: true,
type: csharp.Type.optional(csharp.Type.string()),
summary: BASE_URL_SUMMARY
});

export class RequestOptionsGenerator extends FileGenerator<CSharpFile, SdkCustomConfigSchema, SdkGeneratorContext> {
private baseOptionsGenerator: BaseOptionsGenerator;

constructor(context: SdkGeneratorContext, baseOptionsGenerator: BaseOptionsGenerator) {
super(context);

this.baseOptionsGenerator = baseOptionsGenerator;
}

public doGenerate(): CSharpFile {
const class_ = csharp.class_({
name: REQUEST_OPTIONS_CLASS_NAME,
namespace: this.context.getCoreNamespace(),
partial: true,
access: "public"
});
const optionArgs: OptionArgs = {
optional: true,
includeInitializer: false
};
class_.addField(BASE_URL_FIELD);
class_.addField(this.baseOptionsGenerator.getHttpClientField(optionArgs));
class_.addField(this.baseOptionsGenerator.getMaxRetriesField(optionArgs));
class_.addField(this.baseOptionsGenerator.getTimeoutField(optionArgs));
return new CSharpFile({
clazz: class_,
directory: this.context.getCoreDirectory()
});
}

protected getFilepath(): RelativeFilePath {
return join(
this.context.project.filepaths.getCoreFilesDirectory(),
RelativeFilePath.of(`${REQUEST_OPTIONS_CLASS_NAME}.cs`)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@fern-fern/ir-sdk/api";
import { EndpointGenerator } from "../endpoint/EndpointGenerator";
import { RawClient } from "../endpoint/RawClient";
import { CLIENT_OPTIONS_CLASS_NAME } from "../options/ClientOptionsGenerator";
import { SdkCustomConfigSchema } from "../SdkCustomConfig";
import { SdkGeneratorContext } from "../SdkGeneratorContext";

Expand Down Expand Up @@ -220,7 +221,7 @@ export class RootClientGenerator extends FileGenerator<CSharpFile, SdkCustomConf
csharp.codeblock((writer) => {
writer.writeNode(headerSupplierDictionary);
}),
csharp.codeblock("clientOptions ?? new ClientOptions()")
csharp.codeblock(`clientOptions ?? new ${CLIENT_OPTIONS_CLASS_NAME}()`)
]
})
);
Expand Down
Loading
Loading