Skip to content

feat(swift): generate invocation-only dynamic snippets - #17418

Open
cadesark wants to merge 4 commits into
devin/1786644600-invocation-only-dynamic-snippetsfrom
cade/swift-invocation-snippets
Open

feat(swift): generate invocation-only dynamic snippets#17418
cadesark wants to merge 4 commits into
devin/1786644600-invocation-only-dynamic-snippetsfrom
cade/swift-invocation-snippets

Conversation

@cadesark

@cadesark cadesark commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Stacks on the finalized TS PR #17393. Mirrors the finalized invocation-only dynamic-snippet contract already landed for Python (#17402), Go (#17403), Java (#17404), C# (#17405), PHP (#17407), Ruby (#17408), and Rust (#17409). Swift was accidentally skipped in the first pass, and Swift IS enabled in fern-platform's packages/snippets, so this port has a real consumer.

Contract

generateInvocationSnippetSync returns InvocationSnippetResponse = { snippet, imports, clientName, errors }:

  • snippet — bare invocation only (try await client.endpoints.httpMethods.testGet(id: "id")). No imports, no let client = AcmeClient(...) construction, no private func main() async throws { ... } scaffold, and no trailing terminator (Swift has none; a trailing ; is stripped defensively). Honors options.clientVariableName (default client). Rendered from the underlying Expression (not the discard-assignment Statement, which would prefix _ = ).
  • imports — always "". See the imports decision below.
  • clientName — the generated Swift client class name, from nameRegistry.getRootClientSymbolOrThrow().name (e.g. AcmeClient).
  • errors — preserved from the existing error reporter.

Swift imports decision (flagged)

imports is always the empty string for Swift, matching the Ruby/Rust no-imports pattern (not the C#/Java/PHP { code, imports } helper pattern). The Swift AST (@fern-api/swift-codegen) has no per-node import mechanism: its Writer is a plain string buffer with no getImports/importsToString/addImport. Generated snippets reference every SDK type by a bare name that resolves through the two module-level imports emitted once by the scaffold (import Foundation, import <Module>). Those belong to the client construction the caller owns, not to the invocation, so a bare invocation surfaces no per-symbol import. Inventing an imports mechanism the AST does not have was explicitly avoided.

Implementation

  • Parameterized generateEndpointMethodCallExpression with an optional clientVariableName (falls back to the client default) and added generateInvocationSnippetSync + a getClientName() helper in generators/swift/dynamic-snippets/src/EndpointSnippetGenerator.ts. The full-snippet builder is unchanged.

Tests

generators/swift/dynamic-snippets/src/__test__/InvocationSnippet.test.ts: bare call (exact string, no scaffold/no terminator), imports === "" (incl. a body that constructs typed values to prove Swift still emits none), clientName === "AcmeClient", and custom clientVariableName.

Verification

  • pnpm turbo run compile --filter @fern-api/swift-dynamic-snippets — green.
  • pnpm turbo run test --filter @fern-api/swift-dynamic-snippets — 38 tests pass (5 new + 33 existing, all snapshots unchanged).
  • npx biome check --write on the changed .ts files — clean.
  • Changelog: generators/swift/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml (type: feat). versions.yml untouched.

Generated with Claude Code


Open in Devin Review

cadesark and others added 4 commits August 13, 2026 18:10
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…snippets

The invocation-only dynamic-snippets hook now returns a structured
InvocationSnippetResponse (snippet + imports + clientName + errors) instead of
a bare call string. Callers such as documentation code templates can now
regenerate the imports and client instantiation alongside the call and keep
them in sync across SDK renames.

The branded-string-alias case, which previously returned undefined (dropping
the call because it referenced an SDK import the caller couldn't supply), now
returns the call plus the required import block. generateInvocationSync still
returns undefined only for the "generator doesn't implement the hook"
capability check the multi-language fan-out relies on.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@cadesark
cadesark requested a review from kafkas as a code owner August 14, 2026 13:49
@cadesark cadesark self-assigned this Aug 14, 2026

@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.

AI Review Summary

Adds generateInvocationSnippetSync to Swift's EndpointSnippetGenerator plus tests and a changelog. The core change (parameterizing the client variable name, rendering the bare Expression) is small and reasonable. Main concerns: the public entrypoint wiring isn't in the diff, imports: "" leaves consumers unable to render the client construction they're handed a clientName for, and multi-argument (multiline) invocations aren't covered by an exact-output assertion.

  • 🟡 1 warning(s)
  • 🔵 3 suggestion(s)

};

it("generates the invocation without client construction or scaffold", () => {
const response = generator.generateInvocationSync(request);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

The tests call generator.generateInvocationSync(...) on the DynamicSnippetsGenerator, but no change to DynamicSnippetsGenerator.ts appears in this diff — only EndpointSnippetGenerator.generateInvocationSnippetSync. If generateInvocationSync isn't already implemented and delegating to the new method (e.g. inherited from the base generator), this won't compile/run. Please confirm the wiring is present, or add it.

Comment on lines +101 to +104
// Always empty for Swift — see the method doc comment: the AST has no per-symbol import
// mechanism and types resolve through the scaffold's module-level imports.
imports: "",
clientName: this.getClientName(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

Returning clientName (AcmeClient) while returning imports: "" puts consumers in an awkward spot: they're expected to render let client = AcmeClient(...), which requires import <Module> — and nothing in the response tells them the module name. If the base contract can't carry a module name, consider emitting import Foundation\nimport <Module> here (the caller can dedupe) rather than an empty string, or at minimum note in the doc comment that the consumer must source the module name elsewhere.

Comment on lines +119 to +121
expect(response).not.toBeUndefined();
expect(response?.imports).toBe("");
expect(response?.snippet).not.toContain("import ");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

The only exact-output assertion is a single-argument call. Multi-argument calls take the multiline: true path in generateEndpointMethodCallExpression, which is where leading indentation is most likely to be wrong when rendered outside the func main scaffold. Assert the full expected string for this POST body case (or a snapshot) so indentation regressions get caught.

Comment on lines +59 to +77
/**
* Generates the structured pieces of an endpoint invocation for callers that render the
* invocation within code of their own (e.g. a documentation code template): the bare call
* (e.g. `try await client.endpoints.httpMethods.testGet(id: "id")`, honoring a custom client
* variable name), the imports the call requires, and the generated client class name.
*
* Swift's `imports` is always the empty string. Unlike TypeScript/PHP/C#/Java — whose AST tracks
* per-symbol imports and can surface the `import`/`using` lines a call references — the Swift AST
* (`@fern-api/swift-codegen`) has no per-node import mechanism at all: its {@link swift.Writer}
* is a plain buffer (no `getImports`/`importsToString`/`addImport`), and generated snippets
* reference every SDK type by a bare name that resolves through the two module-level imports
* emitted once by the scaffold — `import Foundation` and `import <Module>` (see
* {@link generateImportFoundationStatement} / {@link generateImportModuleStatement}). Those
* belong to the client construction the caller owns, not to the invocation. A bare invocation
* therefore emits no per-symbol `import`, so we return `""` rather than inventing an imports
* mechanism the language and its AST do not have. This mirrors the Ruby/Rust ports (types
* referenced via the module/gem namespace), not the C#/Java/PHP `{ code, imports }` helper
* pattern.
*/

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

This ~19-line rationale is repeated near-verbatim in the test file header and again in the changelog. Trim to a few lines here ("Swift's AST has no per-node import mechanism; all types resolve via the scaffold's module-level imports, so imports is always """) and drop the duplicates — three copies will drift the first time someone touches this.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1786644600-invocation-only-dynamic-snippets branch from 741c4f6 to c85a00c Compare August 14, 2026 15:44
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