Skip to content

feat(swift): support server URL variable templating - #17354

Open
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1786135877-swift-server-url-templating
Open

feat(swift): support server URL variable templating#17354
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
devin/1786135877-swift-server-url-templating

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

The Swift SDK generator ignored server URL variables: an environment declared as https://api.{region}.example.com/v1 was emitted as the frozen default URL, so {region} could never be changed without passing a full baseURL. Swift and Rust were the last two generators missing this (Rust: #17353); TS/Python/Java/Go/C#/PHP/Ruby already support it.

Templated environments now expose a url(...) method, and the root client takes each variable as an initializer parameter:

public enum ApiEnvironment: String, CaseIterable {
    case regionalApiServer = "https://api.us-east-1.prod.example.com/v1"
}

extension ApiEnvironment {
    /// Returns this environment's URL with the given server URL variables substituted in. …
    public func url(region: String? = nil, environment: String? = nil) -> String {
        switch self {
        case .regionalApiServer:
            return "https://api.\(region ?? "us-east-1").\(environment ?? "prod").example.com/v1"
        }
    }
}

// ApiClient.swift
public convenience init(baseURL: String? = nil, region: String? = nil, environment: String? = nil,) {
    let resolvedBaseURL = baseURL ?? ApiEnvironment.regionalApiServer.url(region: region, environment: environment)
    
}

An explicit baseURL still wins, omitted variables fall back to their IR defaults, and APIs without templates are untouched (baseURL keeps its non-optional, defaulted form). Multiple-base-URL environments are unaffected — Swift doesn't generate environments for them at all yet (TODO(kafkas)), so templating them is a follow-up.

Notable details:

  • Variables are de-duplicated by IR id and filtered to those a template actually references, so an unused urlVariables entry doesn't leak a parameter.
  • A variable whose camelCased name collides with an existing root-client parameter (timeout, token, …) is exposed as serverUrlTimeout rather than shadowing it.
  • Placeholders with no matching variable are emitted literally instead of becoming broken interpolation.

Changes Made

  • Added serverUrlVariables.ts: variable extraction (dedupe, template filtering, collision-safe naming) and URL-template → Swift string-literal rendering.
  • Exposed the variables on SdkGeneratorContext and generated the ApiEnvironment.url(...) extension from SingleUrlEnvironmentGenerator.
  • RootClientGenerator: added the variable parameters, made baseURL optional when templating applies, and resolved it via resolvedBaseURL; factored the default-environment lookup out of getDefaultBaseUrl.
  • Regenerated the server-url-templating-single-url seed fixture.

Testing

  • Unit tests added/updated — serverUrlVariables.test.ts (7 cases: dedupe, casing, unused variables, reserved-name collisions, interpolation with defaults, literal/unterminated placeholders). pnpm turbo run compile test --filter @fern-api/swift-sdk → 38 passed.
  • Manual testing completed — full seed test --generator swift-sdk run: 140/140 fixtures pass, and the only functional diff is the templating fixture (all others byte-identical). The templating fixture also passes with scripts enabled, i.e. swift test -c release in swift:6.1.2.

Link to Devin session: https://app.devin.ai/sessions/ffc45071747143ae905306ba18077836


Open in Devin Review

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from kafkas as a code owner August 7, 2026 21:07
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the changes — everything looks good. No issues found.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +60 to +65
const interpolationsById = new Map(
variables.map(({ variable, name }) => [
variable.id,
`\\(${name} ?? "${escapeSwiftStringLiteralContent(variable.default ?? "")}")`
])
);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Generated Swift SDK fails to build when a server URL placeholder is named like a Swift keyword

The placeholder's name is inserted verbatim into the generated Swift text (${name} at generators/swift/sdk/src/generators/environment/serverUrlVariables.ts:63) without the keyword protection every other generated reference gets, so a URL placeholder called e.g. protocol or default produces source code that will not build.

Impact: Customers whose API URLs use a variable whose name matches a Swift language keyword get an SDK that fails to compile.

Missing reserved-keyword escaping in the generated string interpolation

getServerUrlVariables derives names with caseConverter.camelUnsafe (generators/swift/sdk/src/generators/environment/serverUrlVariables.ts:46,49), i.e. names that are not keyword-escaped; the codegen layer escapes them at write time (Expression.referenceescapeReservedKeyword, generators/swift/codegen/src/ast/Expression.ts:222-223). urlTemplateToStringLiteral bypasses that by building the interpolation as a raw string, so for a variable named protocol the environment extension emits return "https://\(protocol ?? "https")..." instead of the required \(`protocol` ?? "https"), which is not valid Swift. escapeReservedKeyword is exported from @fern-api/swift-codegen (generators/swift/codegen/src/syntax/index.ts) and can be applied here.

Note the same names are also used as parameters in SingleUrlEnvironmentGenerator.generateUrlVariablesExtension and RootClientGenerator.serverUrlVariableParams, where Expression.reference already escapes them — so only the manual interpolation is inconsistent.

Suggested change
const interpolationsById = new Map(
variables.map(({ variable, name }) => [
variable.id,
`\\(${name} ?? "${escapeSwiftStringLiteralContent(variable.default ?? "")}")`
])
);
const interpolationsById = new Map(
variables.map(({ variable, name }) => [
variable.id,
`\\(${escapeReservedKeyword(name)} ?? "${escapeSwiftStringLiteralContent(variable.default ?? "")}")`
])
);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 4d93a03. The interpolation was the one place the name reached generated source without going through Expression.reference's escapeReservedKeyword, so a variable named protocol would have emitted \(protocol ?? "https") — invalid Swift, while the matching parameter declaration was correctly backticked. Now wrapped in escapeReservedKeyword, with a unit test asserting "https://\(protocol ?? "https").example.com". No seed output changes (no fixture uses a keyword-named variable).

…tion

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-07T04:38:17Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
swift-sdk square 62s (n=5) 453s (n=5) 44s -18s (-29.0%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-08-07T04:38:17Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-07 21:39 UTC

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant