feat(swift): generate invocation-only dynamic snippets - #17418
Conversation
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>
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
🟡 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.
| // 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(), |
There was a problem hiding this comment.
🔵 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.
| expect(response).not.toBeUndefined(); | ||
| expect(response?.imports).toBe(""); | ||
| expect(response?.snippet).not.toContain("import "); |
There was a problem hiding this comment.
🔵 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.
| /** | ||
| * 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. | ||
| */ |
There was a problem hiding this comment.
🔵 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.
741c4f6 to
c85a00c
Compare
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
generateInvocationSnippetSyncreturnsInvocationSnippetResponse={ snippet, imports, clientName, errors }:snippet— bare invocation only (try await client.endpoints.httpMethods.testGet(id: "id")). Noimports, nolet client = AcmeClient(...)construction, noprivate func main() async throws { ... }scaffold, and no trailing terminator (Swift has none; a trailing;is stripped defensively). Honorsoptions.clientVariableName(defaultclient). Rendered from the underlyingExpression(not the discard-assignmentStatement, which would prefix_ =).imports— always"". See the imports decision below.clientName— the generated Swift client class name, fromnameRegistry.getRootClientSymbolOrThrow().name(e.g.AcmeClient).errors— preserved from the existing error reporter.Swift imports decision (flagged)
importsis 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: itsWriteris a plain string buffer with nogetImports/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-symbolimport. Inventing an imports mechanism the AST does not have was explicitly avoided.Implementation
generateEndpointMethodCallExpressionwith an optionalclientVariableName(falls back to theclientdefault) and addedgenerateInvocationSnippetSync+ agetClientName()helper ingenerators/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 customclientVariableName.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 --writeon the changed.tsfiles — clean.generators/swift/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml(type: feat).versions.ymluntouched.Generated with Claude Code