feat(ruby): generate invocation-only dynamic snippets - #17408
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>
Mirrors the finalized TypeScript structured contract (PR #17393) and the Python (#17402) / Go (#17403) / Java (#17404) / C# (#17405) / PHP (#17407) ports: alongside the full snippet, the generator now returns InvocationSnippetResponse = { snippet, imports, clientName, errors } for callers (e.g. docs code templates) that render the invocation inside code they already own. - snippet: the bare call (honoring options.clientVariableName), no client construction (`client = Acme::Client.new(...)`), no `require "..."` preamble, and no trailing terminator (Ruby has none). - imports: always the empty string for Ruby. A generated Ruby SDK references every type through the gem's module namespace (e.g. `Acme::Types::Foo`), so a bare invocation never emits a per-symbol `require`. Unlike TS/PHP/C#/Java, Ruby has no import-referencing invocation case, so no imports mechanism is invented for it. The only `require` a snippet emits (`require "acme"`) belongs to the client construction the caller owns. - clientName: the generated client class name (context.getRootClientClassName()). - errors: preserved from the existing error reporter. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
AI Review Summary
Adds generateInvocationSnippetSync to the ruby-v2 endpoint snippet generator plus tests and a changelog entry. Implementation is straightforward and mirrors the sibling ports; main concerns are the changelog landing in the v1 Ruby generator's directory and some doc-comment bloat.
- 🟡 1 warning(s)
- 🔵 2 suggestion(s)
| # yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json | ||
|
|
||
| - summary: | | ||
| Generate invocation-only dynamic snippets. In addition to the full snippet | ||
| (client construction plus the call), the generator now exposes the structured | ||
| pieces a caller can render inside code it already owns (e.g. a documentation | ||
| code template): the bare invocation (`client.endpoints.update(...)`, honoring a | ||
| custom client variable name), the imports the invocation references, and the | ||
| generated client class name so docs can render the client construction and track | ||
| renames. | ||
|
|
||
| For Ruby the imports field is always the empty string: a generated Ruby SDK | ||
| references every type through the gem's module namespace (e.g. | ||
| `Acme::Types::Foo`), so a bare invocation never emits a per-symbol `require`. | ||
| Unlike TypeScript/PHP/C#/Java — where a call that references a type from another | ||
| namespace must emit an import/`use` line — Ruby has no import-referencing | ||
| invocation case to surface, so no imports mechanism is invented for it. The only | ||
| `require` a generated snippet emits (`require "acme"`) belongs to the client | ||
| construction the caller owns, not to the invocation, and is therefore omitted | ||
| from the invocation-only render. | ||
| type: feat |
There was a problem hiding this comment.
🟡 warning
The code change is in generators/ruby-v2/, but this changelog entry is filed under generators/ruby/sdk/changes/ — the v1 Ruby generator. Unless the two share a release stream, this will publish release notes against the wrong package. Please confirm/move to the ruby-v2 generator's changes directory.
| /** | ||
| * 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. `client.plants.update(...)`, honoring a custom client variable name), the imports the | ||
| * call requires, and the generated client class name. | ||
| * | ||
| * Ruby's `imports` is always the empty string. Unlike TypeScript/PHP/C#/Java — where a call | ||
| * that references a type from another namespace must emit a per-symbol import/`use` line — a | ||
| * generated Ruby SDK references every type through the gem's module namespace (e.g. | ||
| * `Acme::Types::Foo`), so a bare invocation never emits a `require`. The generator's only | ||
| * `require` (`require "acme"`, added in {@link constructClient}) belongs to the client | ||
| * construction the caller owns, not to the invocation. There is therefore no import-referencing | ||
| * invocation case to surface for Ruby, so we return `""` rather than inventing an imports | ||
| * mechanism the language does not have. This is the Ruby analogue of Go always emitting | ||
| * `context`, inverted: Ruby's invocation imports are always empty. | ||
| */ |
There was a problem hiding this comment.
🔵 suggestion
This 16-line doc comment restates the same "Ruby has no per-symbol requires" rationale that's also duplicated verbatim in the changelog and the test file header. Two or three lines would carry the same information; the "Ruby analogue of Go always emitting context, inverted" analogy in particular is more likely to confuse a future reader than help.
| // namespace, so a bare invocation emits no per-symbol requires. | ||
| imports: "", | ||
| clientName: this.context.getRootClientClassName(), | ||
| errors: this.context.errors.empty() ? undefined : this.context.errors.toDynamicSnippetErrors() |
There was a problem hiding this comment.
🔵 suggestion
this.context.errors is shared state on the generator's context. If a caller reuses the same EndpointSnippetGenerator for both a full snippet and an invocation snippet (or two invocations), errors from the earlier call will leak into this response. Verify the outer DynamicSnippetsGenerator creates a fresh context per call; if not, scope the errors (e.g. this.context.errors.scope(...)) around callMethod.
| }; | ||
| } | ||
|
|
||
| private buildCodeBlock({ |
There was a problem hiding this comment.
🟡 Client name returned for Ruby docs snippets is missing its module prefix, so rendered client setup code is invalid
The client name handed back for callers to render their own client setup (this.context.getRootClientClassName() at generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts:119) is the bare Client without the gem module prefix, so documentation renders setup code that does not resolve in Ruby.
Impact: Docs and other callers that build the client line from this name produce broken, non-runnable Ruby code.
Unqualified class name vs. the fully-qualified reference used in the full snippet
getRootClientClassName() returns customConfig?.clientModuleName ?? "Client" (generators/ruby-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts:44-46), while the full snippet instantiates the client via getRootClientClassReference(), which wraps that name in the root module (generators/ruby-v2/dynamic-snippets/src/context/DynamicSnippetsGeneratorContext.ts:37-42) and renders Acme::Client.new(...). The contract for InvocationSnippetResponse.clientName states the value is the generated client class/type name so docs can render the construction (generators/browser-compatible-base/src/dynamic-snippets/InvocationSnippetResponse.ts), and the TypeScript port returns the full usable class name (generators/typescript-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts:78). Returning Client yields Client.new(...), which is not the generated client. The new test even asserts the unqualified value (generators/ruby-v2/dynamic-snippets/src/__test__/InvocationSnippet.test.ts:65).
Prompt for agents
In generators/ruby-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts, generateInvocationSnippetSync sets clientName to context.getRootClientClassName(), which returns only the unqualified class name ("Client" by default). Generated Ruby SDK clients are referenced through the gem module namespace (the full snippet uses getRootClientClassReference(), rendering e.g. Acme::Client.new(...)). Because InvocationSnippetResponse.clientName is intended for callers (docs templates) to render the client construction themselves, returning "Client" produces non-resolvable Ruby. Consider returning the module-qualified name (root module + "::" + client class name, e.g. via rendering getRootClientClassReference()) and update the new test expectation in generators/ruby-v2/dynamic-snippets/src/__test__/InvocationSnippet.test.ts accordingly.
Was this helpful? React with 👍 or 👎 to provide feedback.
| # yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json | ||
|
|
||
| - summary: | |
There was a problem hiding this comment.
🟡 Changelog entry for the Ruby generator is filed under the retired generator directory, so the release notes never ship
The new changelog entry is placed under the legacy Ruby generator folder (generators/ruby/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml) instead of the active ruby-v2 generator folder, so the release automation that only watches the active folder never publishes it.
Impact: The feature ships without any user-visible changelog entry or version bump for the Ruby generator.
Release workflow only tracks generators/ruby-v2/sdk/changes
The code changed by this PR lives in generators/ruby-v2/, and the active generator's changelog/versions live in generators/ruby-v2/sdk/changes/** and generators/ruby-v2/sdk/versions.yml. .github/workflows/release-software.yml:15 watches only generators/ruby-v2/sdk/changes/** (with ruby-v2 in the generator list at line 37); generators/ruby/sdk contains nothing but a stale changes directory. REVIEW.md requires generator feature changes to include an entry in the appropriate generator directory.
Prompt for agents
The changelog file was added at generators/ruby/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml, but the code change targets the ruby-v2 generator. Move the entry to generators/ruby-v2/sdk/changes/unreleased/ (the directory watched by .github/workflows/release-software.yml) so it is picked up by release automation, keeping the schema-relative path correct for its new location.
Was this helpful? React with 👍 or 👎 to provide feedback.
741c4f6 to
c85a00c
Compare
Stacked on the TypeScript invocation-only PR (#17393). Mirrors the finalized structured contract and the Python (#17402) / Go (#17403) / Java (#17404) / C# (#17405) / PHP (#17407) ports for the Ruby (
ruby-v2) generator.What
Alongside the full snippet,
EndpointSnippetGeneratornow implements the optionalgenerateInvocationSnippetSynchook, returningInvocationSnippetResponse = { snippet, imports, clientName, errors }for callers (e.g. docs code templates) that render the invocation inside code they already own.snippet— the bare call, honoringoptions.clientVariableName(defaultclient). No client construction (client = Acme::Client.new(...)), norequire "..."preamble, no trailing terminator (Ruby has none). The invocation node is rendered in isolation viacallMethod, parameterized withclientVariableName.imports— always""for Ruby. A generated Ruby SDK references every type through the gem's module namespace (e.g.Acme::Types::Foo), so a bare invocation never emits a per-symbolrequire.addRequireis called in exactly one place in the codebase —constructClient, for the rootrequire "acme"line — which belongs to the client construction the caller owns, not the invocation. Unlike TS/PHP/C#/Java there is no import-referencing invocation case to surface, so norenderNodeWithoutImports/toStringWithoutImportsAST helper was added; inventing one would model a mechanism Ruby does not have. This is documented in a code comment, the changelog, and the test. (The Ruby analogue of Go always emittingcontext, inverted: Ruby's invocation imports are always empty.)clientName—context.getRootClientClassName().errors— preserved from the existing error reporter.Ruby imports decision (flagged)
Ruby returns
imports: ""in all cases, including invocations that construct typed body values (datetime, uuid, nested collections). This is the expected/correct result. No AST changes were required.Tests
New
generators/ruby-v2/dynamic-snippets/src/__test__/InvocationSnippet.test.ts(5 tests, all green):client.endpoints.http_methods.test_get(id: "id")), asserting no.new(and norequireimports === ""clientName === "Client"clientVariableName(mailchimp.endpoints.http_methods.test_get(id: "id"))imports === ""(documents that Ruby has no import-referencing case)Existing suite green:
@fern-api/ruby-dynamic-snippets38 tests pass, no snapshot changes.Verification
pnpm turbo run compile --filter @fern-api/ruby-dynamic-snippets(+ ruby-ast) — cleanpnpm turbo run test --filter @fern-api/ruby-dynamic-snippets— 38 passed, snapshots unchangedbiome check --writeon changed.ts— no fixes neededgenerators/ruby/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml(type: feat)Generated with Claude Code