Skip to content

feat(ruby): generate invocation-only dynamic snippets - #17408

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

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

Conversation

@cadesark

@cadesark cadesark commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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, EndpointSnippetGenerator now implements the optional generateInvocationSnippetSync hook, returning 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 (default client). No client construction (client = Acme::Client.new(...)), no require "..." preamble, no trailing terminator (Ruby has none). The invocation node is rendered in isolation via callMethod, parameterized with clientVariableName.
  • importsalways "" 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. addRequire is called in exactly one place in the codebase — constructClient, for the root require "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 no renderNodeWithoutImports/toStringWithoutImports AST 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 emitting context, inverted: Ruby's invocation imports are always empty.)
  • clientNamecontext.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):

  • bare call exact string (client.endpoints.http_methods.test_get(id: "id")), asserting no .new( and no require
  • imports === ""
  • clientName === "Client"
  • custom clientVariableName (mailchimp.endpoints.http_methods.test_get(id: "id"))
  • a typed-body invocation still returns imports === "" (documents that Ruby has no import-referencing case)

Existing suite green: @fern-api/ruby-dynamic-snippets 38 tests pass, no snapshot changes.

Verification

  • pnpm turbo run compile --filter @fern-api/ruby-dynamic-snippets (+ ruby-ast) — clean
  • pnpm turbo run test --filter @fern-api/ruby-dynamic-snippets — 38 passed, snapshots unchanged
  • biome check --write on changed .ts — no fixes needed
  • Changelog: generators/ruby/sdk/changes/unreleased/invocation-only-dynamic-snippets.yml (type: feat)

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>
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>
@cadesark cadesark self-assigned this Aug 13, 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 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)

Comment on lines +1 to +21
# 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

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

Comment on lines +68 to +83
/**
* 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.
*/

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 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()

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

@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 found 2 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

};
}

private buildCodeBlock({

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.

🟡 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.
Open in Devin Review

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

Comment on lines +1 to +3
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |

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.

🟡 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.
Open in Devin Review

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

@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