Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ export abstract class AbstractSwiftGeneratorContext<
});
nameRegistry.registerEnvironmentSymbol({
configEnvironmentEnumName: this.customConfig.environmentEnumName,
registeredSourceModuleName: registeredSourceModuleSymbol.name
registeredSourceModuleName: registeredSourceModuleSymbol.name,
environmentType:
ir.environments?.environments.type === "multipleBaseUrls" ? "multipleBaseUrls" : "singleBaseUrl"
});

// Must first register top-level symbols
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public final class ClientConfig: Swift.Sendable {
}

let baseURL: Swift.String
let baseUrls: [Swift.String: Swift.String]?
let headerAuth: HeaderAuth?
let bearerAuth: BearerAuth?
let basicAuth: BasicAuth?
Expand All @@ -56,6 +57,7 @@ public final class ClientConfig: Swift.Sendable {

init(
baseURL: Swift.String,
baseUrls: [Swift.String: Swift.String]? = nil,
headerAuth: HeaderAuth? = nil,
bearerAuth: BearerAuth? = nil,
basicAuth: BasicAuth? = nil,
Expand All @@ -65,6 +67,7 @@ public final class ClientConfig: Swift.Sendable {
urlSession: Networking.URLSession? = nil
) {
self.baseURL = baseURL
self.baseUrls = baseUrls
self.headerAuth = headerAuth
self.bearerAuth = bearerAuth
self.basicAuth = basicAuth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class HTTPClient: Swift.Sendable {
func performRequest(
method: HTTP.Method,
path: Swift.String,
baseUrlId: Swift.String? = nil,
contentType requestContentType: HTTP.ContentType = .applicationJson,
headers requestHeaders: [Swift.String: Swift.String?] = [:],
queryParams requestQueryParams: [Swift.String: QueryParameter?] = [:],
Expand All @@ -26,6 +27,7 @@ final class HTTPClient: Swift.Sendable {
_ = try await performRequest(
method: method,
path: path,
baseUrlId: baseUrlId,
contentType: requestContentType,
headers: requestHeaders,
queryParams: requestQueryParams,
Expand All @@ -39,6 +41,7 @@ final class HTTPClient: Swift.Sendable {
func performRequest<T: Swift.Decodable>(
method: HTTP.Method,
path: Swift.String,
baseUrlId: Swift.String? = nil,
contentType requestContentType: HTTP.ContentType = .applicationJson,
headers requestHeaders: [Swift.String: Swift.String?] = [:],
queryParams requestQueryParams: [Swift.String: QueryParameter?] = [:],
Expand All @@ -61,6 +64,7 @@ final class HTTPClient: Swift.Sendable {
let request = try await buildRequest(
method: method,
path: path,
baseUrlId: baseUrlId,
requestContentType: requestContentType,
requestHeaders: requestHeaders,
requestQueryParams: requestQueryParams,
Expand Down Expand Up @@ -99,6 +103,7 @@ final class HTTPClient: Swift.Sendable {
private func buildRequest(
method: HTTP.Method,
path: Swift.String,
baseUrlId: Swift.String? = nil,
requestContentType: HTTP.ContentType,
requestHeaders: [Swift.String: Swift.String?],
requestQueryParams: [Swift.String: QueryParameter?],
Expand All @@ -107,7 +112,7 @@ final class HTTPClient: Swift.Sendable {
) async throws -> Networking.URLRequest {
// Init with URL
let url = buildRequestURL(
path: path, requestQueryParams: requestQueryParams, requestOptions: requestOptions
path: path, baseUrlId: baseUrlId, requestQueryParams: requestQueryParams, requestOptions: requestOptions
)
var request = Networking.URLRequest(url: url)

Expand Down Expand Up @@ -143,10 +148,12 @@ final class HTTPClient: Swift.Sendable {

private func buildRequestURL(
path: Swift.String,
baseUrlId: Swift.String? = nil,
requestQueryParams: [Swift.String: QueryParameter?],
requestOptions: RequestOptions? = nil
) -> URL {
let endpointURL = "\(clientConfig.baseURL)\(path)"
let baseURL = baseUrlId.flatMap { clientConfig.baseUrls?[$0] } ?? clientConfig.baseURL
let endpointURL = "\(baseURL)\(path)"
guard var components = Foundation.URLComponents(string: endpointURL) else {
preconditionFailure(
"Invalid URL '\(endpointURL)' - this indicates an unexpected error in the SDK."
Expand Down
11 changes: 9 additions & 2 deletions generators/swift/codegen/src/name-registry/name-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,12 @@ export class NameRegistry {
*/
public registerEnvironmentSymbol({
configEnvironmentEnumName,
registeredSourceModuleName
registeredSourceModuleName,
environmentType
}: {
configEnvironmentEnumName: string | undefined;
registeredSourceModuleName: string;
environmentType: "singleBaseUrl" | "multipleBaseUrls";
}): swift.Symbol {
const candidates: [string, ...string[]] = [
`${registeredSourceModuleName}Environment`,
Expand All @@ -197,7 +199,12 @@ export class NameRegistry {
candidates.unshift(configEnvironmentEnumName);
}
const symbolName = this.sourceModuleNamespace.addEnvironmentSymbolName(candidates);
return this.symbolRegistry.registerSourceModuleType(symbolName, { type: "enum-with-raw-values" });
// Environments with multiple base URLs are generated as a struct (one property per base URL),
// whereas environments with a single base URL are generated as a String-backed enum.
return this.symbolRegistry.registerSourceModuleType(
symbolName,
environmentType === "multipleBaseUrls" ? { type: "struct" } : { type: "enum-with-raw-values" }
);
}

public getEnvironmentSymbolOrThrow(): swift.Symbol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ export class DynamicSnippetsGeneratorContext extends AbstractDynamicSnippetsGene
});
nameRegistry.registerEnvironmentSymbol({
configEnvironmentEnumName: this.customConfig?.environmentEnumName,
registeredSourceModuleName: registeredSourceModuleSymbol.name
registeredSourceModuleName: registeredSourceModuleSymbol.name,
environmentType:
ir.environments?.environments.type === "multipleBaseUrls" ? "multipleBaseUrls" : "singleBaseUrl"
});

// Must first register top-level symbols
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Add support for APIs with multiple base URLs (server URL templating). The environment type is now generated as a struct exposing one URL per base URL ID, and each endpoint resolves its configured base URL at request time.
type: fix
16 changes: 14 additions & 2 deletions generators/swift/sdk/src/SdkGeneratorCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FernIr } from "@fern-fern/ir-sdk";
import { template as templateFn } from "lodash-es";

import {
MultipleBaseUrlsEnvironmentGenerator,
PackageSwiftGenerator,
RootClientGenerator,
SingleUrlEnvironmentGenerator,
Expand Down Expand Up @@ -640,8 +641,19 @@ export class SdkGeneratorCLI extends AbstractSwiftGeneratorCli<SdkCustomConfigSc
directory: RelativeFilePath.of(""),
contents: [environmentEnum]
});
} else {
// TODO(kafkas): Handle multiple environments
} else if (context.ir.environments && context.ir.environments.environments.type === "multipleBaseUrls") {
const environmentSymbol = context.project.nameRegistry.getEnvironmentSymbolOrThrow();
const environmentGenerator = new MultipleBaseUrlsEnvironmentGenerator({
structName: environmentSymbol.name,
environments: context.ir.environments.environments,
sdkGeneratorContext: context
});
const environmentStruct = environmentGenerator.generate();
context.project.addSourceFile({
nameCandidateWithoutExtension: environmentStruct.name,
directory: RelativeFilePath.of(""),
contents: [environmentStruct]
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,6 @@ export class EndpointMethodGenerator {
label: "method",
value: swift.Expression.enumCaseShorthand(this.getEnumCaseNameForHttpMethod(endpoint.method))
}),
// TODO(kafkas): Handle multi-url environments
swift.functionArgument({
label: "path",
value: swift.Expression.stringLiteral(
Expand All @@ -254,6 +253,11 @@ export class EndpointMethodGenerator {
})
];

const baseUrlIdArgument = this.getBaseUrlIdArgumentForEndpoint(endpoint);
if (baseUrlIdArgument != null) {
arguments_.push(baseUrlIdArgument);
}

if (endpoint.requestBody?.type === "bytes") {
arguments_.push(
swift.functionArgument({
Expand Down Expand Up @@ -431,6 +435,26 @@ export class EndpointMethodGenerator {
return arguments_;
}

/**
* For APIs with multiple base URLs (server URL templating), an endpoint may declare which base
* URL it targets. When it does, we pass the corresponding base URL ID so the HTTP client can
* resolve the correct URL from the configured environment at request time.
*/
private getBaseUrlIdArgumentForEndpoint(endpoint: FernIr.HttpEndpoint): swift.FunctionArgument | undefined {
const environments = this.sdkGeneratorContext.ir.environments?.environments;
if (endpoint.baseUrl == null || environments?.type !== "multipleBaseUrls") {
return undefined;
}
const baseUrl = environments.baseUrls.find((b) => b.id === endpoint.baseUrl);
if (baseUrl == null) {
return undefined;
}
return swift.functionArgument({
label: "baseUrlId",
value: swift.Expression.stringLiteral(baseUrl.id)
});
}

private getResolvedSwiftTypeForTypeReference(typeReference: FernIr.TypeReference): swift.TypeReference {
if (typeReference.type === "named") {
const { typeId } = typeReference;
Expand Down
115 changes: 103 additions & 12 deletions generators/swift/sdk/src/generators/client/RootClientGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,102 @@ export class RootClientGenerator {
});
}

private get environmentParam() {
const environmentSymbol = this.sdkGeneratorContext.project.nameRegistry.getEnvironmentSymbolOrThrow();
return swift.functionParameter({
argumentLabel: "environment",
unsafeName: "environment",
type: this.referencer.referenceType(environmentSymbol),
defaultValue: this.getDefaultEnvironmentExpression(),
docsContent:
"The environment to use for requests from the client. If not provided, the default environment will be used."
});
}

private get isMultipleBaseUrls(): boolean {
return this.sdkGeneratorContext.ir.environments?.environments.type === "multipleBaseUrls";
}

/**
* The base-URL-related initializer parameters. APIs with a single base URL expose a `baseURL`
* string, whereas APIs with multiple base URLs (server URL templating) expose an `environment`
* parameter typed as the generated environment struct.
*/
private getBaseUrlParams(): swift.FunctionParameter[] {
return this.isMultipleBaseUrls ? [this.environmentParam] : [this.baseUrlParam];
}

/**
* The arguments forwarded from a convenience initializer to the designated initializer for the
* base-URL-related parameters.
*/
private getBaseUrlForwardingArgs(): swift.FunctionArgument[] {
if (this.isMultipleBaseUrls) {
return [swift.functionArgument({ label: "environment", value: swift.Expression.reference("environment") })];
}
return [swift.functionArgument({ label: "baseURL", value: swift.Expression.reference("baseURL") })];
}

/**
* The arguments passed to `ClientConfig` for the base-URL-related parameters. For multiple base
* URLs we both set a fallback `baseURL` (the first base URL) and a `baseUrls` map keyed by base
* URL ID so that individual endpoints can resolve the correct URL at request time.
*/
private getBaseUrlClientConfigArgs(): swift.FunctionArgument[] {
const environments = this.sdkGeneratorContext.ir.environments?.environments;
if (environments == null || environments.type !== "multipleBaseUrls") {
return [swift.functionArgument({ label: "baseURL", value: swift.Expression.reference("baseURL") })];
}
const baseUrlProperty = (baseUrl: FernIr.EnvironmentBaseUrlWithId) =>
swift.Expression.memberAccess({
target: swift.Expression.reference("environment"),
memberName: this.sdkGeneratorContext.caseConverter.camelUnsafe(baseUrl.name)
});
const args: swift.FunctionArgument[] = [];
const firstBaseUrl = environments.baseUrls[0];
if (firstBaseUrl != null) {
args.push(swift.functionArgument({ label: "baseURL", value: baseUrlProperty(firstBaseUrl) }));
}
args.push(
swift.functionArgument({
label: "baseUrls",
value: swift.Expression.dictionaryLiteral({
entries: environments.baseUrls.map((baseUrl) => [
swift.Expression.stringLiteral(baseUrl.id),
baseUrlProperty(baseUrl)
]),
multiline: true
})
})
);
return args;
}

private getDefaultEnvironmentExpression(): swift.Expression | undefined {
const envConfig = this.sdkGeneratorContext.ir.environments;
if (envConfig == null || envConfig.environments.type !== "multipleBaseUrls") {
return undefined;
}
const defaultEnvId = envConfig.defaultEnvironment;

// If no default environment is specified, use the first environment (mirrors single-base-URL behavior).
const defaultEnvironment = envConfig.environments.environments.find((e, idx) =>
defaultEnvId == null ? idx === 0 : e.id === defaultEnvId
);
if (defaultEnvironment == null) {
return undefined;
}
const environmentSymbol = this.sdkGeneratorContext.project.nameRegistry.getEnvironmentSymbolOrThrow();
const environmentRef = this.sdkGeneratorContext.project.nameRegistry.reference({
fromSymbol: this.symbol,
toSymbol: environmentSymbol
});
return swift.Expression.memberAccess({
target: swift.Expression.reference(environmentRef),
memberName: this.sdkGeneratorContext.caseConverter.camelUnsafe(defaultEnvironment.name)
});
}

private get headersParam() {
return swift.functionParameter({
argumentLabel: "headers",
Expand Down Expand Up @@ -166,10 +262,7 @@ export class RootClientGenerator {
: swift.Expression.reference("headers");

const designatedInitializerArgs: swift.FunctionArgument[] = [
swift.functionArgument({
label: "baseURL",
value: swift.Expression.reference("baseURL")
}),
...this.getBaseUrlForwardingArgs(),
swift.functionArgument({
label: "headerAuth",
value: authSchemes.header
Expand Down Expand Up @@ -389,7 +482,7 @@ export class RootClientGenerator {
}: {
bearerTokenParamType: BearerTokenParamType;
}): swift.FunctionParameter[] {
const params: swift.FunctionParameter[] = [this.baseUrlParam];
const params: swift.FunctionParameter[] = [...this.getBaseUrlParams()];
const authSchemes = this.getAuthSchemeParameters();
if (authSchemes.header) {
params.push(authSchemes.header.param);
Expand All @@ -413,12 +506,7 @@ export class RootClientGenerator {
}

private generateDesignatedInitializer(): swift.Initializer {
const initializerParams: swift.FunctionParameter[] = [
swift.functionParameter({
argumentLabel: "baseURL",
unsafeName: "baseURL",
type: this.referencer.referenceSwiftType("String")
}),
const otherParams: swift.FunctionParameter[] = [
swift.functionParameter({
argumentLabel: "headerAuth",
unsafeName: "headerAuth",
Expand Down Expand Up @@ -485,6 +573,8 @@ export class RootClientGenerator {
})
];

const initializerParams: swift.FunctionParameter[] = [...this.getBaseUrlParams(), ...otherParams];

return swift.initializer({
parameters: initializerParams,
body: swift.CodeBlock.withStatements([
Expand All @@ -493,7 +583,8 @@ export class RootClientGenerator {
value: swift.Expression.classInitialization({
unsafeName: "ClientConfig",
arguments_: [
...initializerParams.map((p) =>
...this.getBaseUrlClientConfigArgs(),
...otherParams.map((p) =>
swift.functionArgument({
label: p.unsafeName,
value: swift.Expression.reference(p.unsafeName)
Expand Down
Loading