Skip to content

feat(ruby): IR-driven idempotency key generation#17008

Open
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1783699100-ruby-idempotency-key
Open

feat(ruby): IR-driven idempotency key generation#17008
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1783699100-ruby-idempotency-key

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description

Makes idempotency-key generation in the modern Ruby generator (generators/ruby-v2) IR-driven, mirroring the TS reference PR #17007.

Important

Depends on #17025 (chore(ruby): upgrade Ruby SDK generator to IR v67). This PR is based on that branch and its base is set to it. sdkConfig.idempotencyKeyGeneration is only fed to the generator once ruby-v2 consumes IR v67, so this goes CI-green once #17025 merges. The diff shown here is idempotency-only.

Instead of a per-generator config flag with hardcoded POST/PUT, the generator now reads ir.sdkConfig.idempotencyKeyGeneration (optional block with headerName: string and methods: HttpMethod[]). Field present = enabled; absent = disabled. headerName supplies the header; methods gates which HTTP methods get it (no hardcoding).

Runtime behavior is preserved: the caller-supplied key wins, otherwise a UUIDv4 is generated (SecureRandom.uuid). With idempotency disabled, output is byte-identical to main.

Changes Made

  • HttpEndpointGenerator.ts — new getAutoGeneratedIdempotencyKey({ endpoint, requestType }) reads context.ir.sdkConfig.idempotencyKeyGeneration, gates on the configured methods and on request type (json/multipartform), and skips injection if the endpoint already declares the header. Emits caller-wins fallback request_options[:idempotency_key] || SecureRandom.uuid into the existing header bag (or a new one). Removed the old config-flag + hardcoded-method plumbing.
  • packages/commons/api-workspace-commons/src/checkVersionExists.ts — brought in the shared seed-harness fix verbatim from the TS reference: getGeneratorConfigObject() falls back to generatorInvocation.config when raw.config is absent, used by getIdempotencyKeyGenerationFromGeneratorConfig(). Added unit tests.
  • Seed fixtureidempotency-headers in seed/ruby-sdk-v2/seed.yml with no-custom-config (baseline, no injection) and auto-generate-idempotency-key (enabled). Regenerated: enabled create (POST) emits headers = { "Idempotency-Key" => request_options[:idempotency_key] || SecureRandom.uuid }; delete (DELETE) untouched; baseline emits no injection.
  • Changelogfeat entry under generators/ruby-v2/sdk/changes/unreleased/.
  • Updated README.md generator (if applicable) — n/a

Testing

  • Unit tests added/updated — @fern-api/api-workspace-commons suite passes.
  • Manual testing completed — pnpm seed test --generator ruby-sdk-v2 --fixture idempotency-headers --local --skip-scripts → 2/2 pass; injection confirmed on POST only, caller-key-wins fallback verified.

Link to Devin session: https://app.devin.ai/sessions/dcab1efb11c3438884ebf517443d6b5b
Requested by: @cadesark


Open in Devin Review

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

AI Review Summary

PR ports auto-generated idempotency key support to the Ruby v2 generator. The core logic is sound and correctly gated behind an opt-in flag. One correctness concern: the fallback expression references request_options[:idempotency_key], which may not match how request options are actually keyed/threaded in generated methods.

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

this.context.ir.idempotencyHeaders.some(
(header) => getWireValue(header.name).toLowerCase() === IDEMPOTENCY_KEY_HEADER.toLowerCase()
);
return declaresIdempotencyKey ? `request_options[:idempotency_key] || SecureRandom.uuid` : `SecureRandom.uuid`;

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

request_options[:idempotency_key] assumes request_options is in scope as a local variable at the point where the header bag is built, and that the caller's idempotency value is keyed under :idempotency_key. Confirm both: (1) request_options is always the accessible local at this injection site (the generated create method), and (2) the declared idempotency header actually surfaces via request_options[:idempotency_key] rather than through the normal header-parameter code block. If the header is instead emitted as a named method param, this fallback silently ignores the caller's value. The committed fixture only shows the flag-on create output — worth double-checking the standalone-vs-fallback branch is exercised.

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.

Verified both points against the generated output — not a bug:

  1. request_options is always in scope. Every generated endpoint method has the signature def <name>(request_options: {}, **params), and the injection site sits inside that method body. It's the same local used for request_options[:base_url] elsewhere in the same method.

  2. :idempotency_key is a valid caller-wins key. In ruby-v2, request_options is an open hash (default {}), read via []/dig — there's no typed RequestOptions struct restricting keys (unlike the TS IdempotentRequestOptions.idempotencyKey). So client.payment.create(request_options: { idempotency_key: "abc" }, ...) sets the header and the generated UUID is only the fallback. This is the idiomatic Ruby analog of the TS requestOptions?.idempotencyKey ?? generateIdempotencyKey().

Additionally, the generic override path still works and is unchanged: the header bag flows into sdk_headers and merge_additional_headers merges request_options[:additional_headers] on top (Idempotency-Key is not in protected_keys), so a caller can also override via additional_headers.

On the standalone-vs-fallback branch: the shared idempotency-headers test-definition only declares an idempotent POST (create) plus a DELETE, so the committed fixture exercises the declared/fallback branch and the never-inject branch (DELETE). The standalone SecureRandom.uuid branch (plain POST/PUT with no declared Idempotency-Key) is reached by the same code path; I intentionally didn't add a plain-POST endpoint to the shared definition because that would force regeneration across every generator's fixtures — the TS reference PR #17007 didn't add one either.

this.context.ir.idempotencyHeaders.some(
(header) => getWireValue(header.name).toLowerCase() === IDEMPOTENCY_KEY_HEADER.toLowerCase()
);
return declaresIdempotencyKey ? `request_options[:idempotency_key] || SecureRandom.uuid` : `SecureRandom.uuid`;

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

Template literals with no interpolation — plain string literals are cleaner:

Suggested change
return declaresIdempotencyKey ? `request_options[:idempotency_key] || SecureRandom.uuid` : `SecureRandom.uuid`;
return declaresIdempotencyKey ? "request_options[:idempotency_key] || SecureRandom.uuid" : "SecureRandom.uuid";

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.

Done — switched to plain string literals in fb27aae. Generated output is byte-identical (same string), so no fixture change.

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

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

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

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 76s (n=5) 115s (n=5) 62s -14s (-18.4%)
go-sdk square 139s (n=5) 285s (n=5) 134s -5s (-3.6%)
java-sdk square 226s (n=5) 267s (n=5) 191s -35s (-15.5%)
php-sdk square 61s (n=5) 85s (n=5) 63s +2s (+3.3%)
python-sdk square 135s (n=5) 241s (n=5) 131s -4s (-3.0%)
ruby-sdk-v2 square 94s (n=5) 124s (n=5) 79s -15s (-16.0%)
rust-sdk square 169s (n=5) 176s (n=5) 167s -2s (-1.2%)
swift-sdk square 58s (n=5) 433s (n=5) 51s -7s (-12.1%)
ts-sdk square 127s (n=5) 128s (n=5) 123s -4s (-3.1%)

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-07-13T05:08:13Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-13 23:29 UTC

@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1783699100-ruby-idempotency-key branch from fb27aae to 0b89ce8 Compare July 10, 2026 22:55
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
cade and others added 2 commits July 10, 2026 23:06
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>
@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1783699100-ruby-idempotency-key branch from 0b89ce8 to 79cee78 Compare July 10, 2026 23:06
@devin-ai-integration devin-ai-integration Bot changed the base branch from main to devin/1783724254-ruby-ir67-upgrade July 10, 2026 23:06
@devin-ai-integration devin-ai-integration Bot changed the title feat(ruby): auto-generate idempotency key for POST/PUT feat(ruby): IR-driven idempotency key generation Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

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

Fixture main PR Delta
docs 241.5s (n=5) 212.7s (32 versions) -28.8s (-11.9%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 32 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-07-13T05:08:13Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-07-13 23:28 UTC

@devin-ai-integration devin-ai-integration Bot force-pushed the devin/1783699100-ruby-idempotency-key branch from b4bbefb to 79cee78 Compare July 11, 2026 00:09

@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

The PR makes idempotency-key generation IR-driven in the Ruby generator and fixes the seed-harness config fallback. Core logic looks sound. One concern about the generated header-injection ordering relative to additional_headers merge behavior, and a minor duplicate-key case-sensitivity detail worth confirming.

  • 🔵 1 suggestion(s)

// If a declared endpoint header already carries the configured wire name it is
// emitted by the header code block; avoid a duplicate entry.
const hasEndpointIdempotencyKeyHeader = endpoint.headers.some(
(header) => getWireValue(header.name).toLowerCase() === headerName.toLowerCase()

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

Case-insensitive matching against endpoint headers is good, but note that when you do inject (headerName from config) you emit the exact configured casing as the hash key. If the underlying request bag later merges additional_headers with case-insensitive dedup (as merge_additional_headers does), a caller passing a differently-cased variant would be filtered — which is probably the intent, just confirm the two casings line up. No change required if that's expected.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Base automatically changed from devin/1783724254-ruby-ir67-upgrade to main July 13, 2026 16:03
devin-ai-integration Bot and others added 2 commits July 13, 2026 17:29
… idempotency resolver conflict in favor of main (global api.settings support)

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

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

3 similar comments
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

…in a core helper

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.

0 participants