feat(rust): respect an omittable request body behind respectOptionalRequestBody - #17400
feat(rust): respect an omittable request body behind respectOptionalRequestBody#17400devin-ai-integration[bot] wants to merge 11 commits into
Conversation
…uire Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Each generator now decides what to do with an example that sends no body: TypeScript and C# skip it because their generated request type requires the body, and Java's snippets build an empty body for the required wrapper field. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… value 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>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…equestBody 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 EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| private callOmitsRequestBody({ snippet }: { snippet: FernIr.dynamic.EndpointSnippetRequest }): boolean { | ||
| const value = snippet.requestBody; | ||
| return value == null || (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0); | ||
| } |
There was a problem hiding this comment.
🟡 Examples that send an empty request body are rendered as sending no body at all
An example whose request body is an empty object is treated as having no body at all (callOmitsRequestBody at generators/rust/dynamic-snippets/src/EndpointSnippetGenerator.ts:623-626), so the generated Rust snippet passes None and the call sends no body and no content type instead of an empty body.
Impact: Snippets, docs and wire tests for such examples show a call that skips the body entirely, misrepresenting what the API expects to receive.
Why an explicitly empty example body is indistinguishable from an absent one
When respectOptionalRequestBody is on and the dynamic request reports bodyRequired === false with a typeReference body, getMethodArgsForBodyRequest (generators/rust/dynamic-snippets/src/EndpointSnippetGenerator.ts:590-597) pushes rust.Expression.raw("None") whenever callOmitsRequestBody returns true. That helper returns true not only for snippet.requestBody == null but also for any non-array object with zero keys, i.e. {}.
An example that explicitly supplies {} for a body type whose properties are all optional (a legitimate, representable example) is therefore rendered as client.method(None, None) rather than client.method(Some(&Body { .. }), None). Restricting the check to value == null would keep an explicit empty body distinct from an omitted one.
| private callOmitsRequestBody({ snippet }: { snippet: FernIr.dynamic.EndpointSnippetRequest }): boolean { | |
| const value = snippet.requestBody; | |
| return value == null || (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0); | |
| } | |
| private callOmitsRequestBody({ snippet }: { snippet: FernIr.dynamic.EndpointSnippetRequest }): boolean { | |
| return snippet.requestBody == null; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional, and consistent across the languages that already opted in: the OpenAPI importer spells an example that omits the body as an empty object as well as as an absent one, so treating only null as "omitted" would make the bodyless example render Some(&Body { .. }) while the SDK signature says the body may be left out — which is the exact defect I fixed in TS/C#/Go earlier in this series. Go's callOmitsRequestBody applies the same null-or-empty-object check (generators/go-v2/dynamic-snippets/src/EndpointSnippetGenerator.ts:960) and pins it with a test that asserts both undefined and {} produce nil (generators/go-v2/dynamic-snippets/src/__test__/OptionalRequestBody.test.ts:64).
A body type whose properties are all optional and whose example is explicitly {} is indistinguishable from an omitted one at this layer, agreed — but {} and no body are wire-equivalent only in the second case, so the tradeoff was resolved in favor of the omission the flag exists to express. Not changing here; if we want to distinguish them it should change in all four generators at once, driven by the importer keeping the distinction.
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>
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
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 |
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
Description
Rust's turn at the opt-in from #17386's IR change: an endpoint whose request body the API does not
mark as required can now take that body as an
Option, so a caller who passesNonesends neither abody nor a
Content-Type. Reading the IR field is gated on a newrespectOptionalRequestBodyconfig, so every existing Rust SDK keeps the signatures it has today.
Only a body the caller passes on its own is eligible:
A body the IR folds into a wrapper request stays required — the wrapper is what the caller passes,
and the dynamic IR carries no omittability for it, so a snippet could not agree with the signature.
Generated shape with the flag on:
The body argument is serialized as
request.map(serde_json::to_value).transpose()?, soNonereaches
execute_requestasNoneinstead ofSome(json!({})).bodyRequiredis also carried from the SDK IR into the dynamic IR'sjustRequestBodyrequest, sothe snippet generator — which never sees the SDK IR — can tell an omittable body from a required one.
This also moves the Rust generators onto IR v67 (
@fern-fern/ir-sdk/@fern-api/dynamic-ir-sdk67.21.0), which is whererequiredandbodyRequiredlive, and registers the first Rust generatorversions that consume v67 in the v67→v66 migration.
Depends on #17386 (merged into this branch); its example-preservation change is what makes a bodyless
example reach the generator at all.
Changes Made
respectOptionalRequestBody(defaultfalse) inBaseRustConfigSchema, plus amayOmitRequestBodyhelper shared by the client, reference, and snippet pathsSubClientGenerator:Option<&T>parameter and conditional body serializationReferenceConfigAssembler:request: Option<T>in reference.md signaturesEndpointSnippetGenerator:Nonefor an example that omits the body,Some(&body)otherwiseDynamicSnippetsConverter: propagatebodyRequiredforjustRequestBodyrequestsclientCredentialsnarrowing anddefaultValuethe newIR types require), seed
irVersion: v67, and Rust entries in the v67→v66 migrationrespect-optional-request-bodytest definition, generated with and without the flagTesting
pnpm seed test --generator rust-sdk --fixture respect-optional-request-body→ 2/2. With the flag,bulk_refundtakesOption<&RefundRequest>and example5 isclient.bulk_refund(None, None); withoutit, the same example renders
&RefundRequest { ..Default::default() }and the signature is unchanged.cargo build,cargo fmt --check,cargo clippypass on the fixture output, and the generatedsnippets compile as examples against the crate.
Generated a real customer config (Payabli,
POST /v2/MoneyIn/refund/{transId},requestBody.required: false) with the flag on:with
client.money_in.refundv_2(&"10-3ffa27df-...".to_string(), None, None)in the bodyless snippet,doc comment, and reference.md, and the bodyful split-refund snippets still passing the request. The
crate and that snippet both
cargo build.Link to Devin session: https://app.devin.ai/sessions/c55e670b263542b288507a66f8898dac