[rust] fix: select response entity variant by status code

TE
3d ago
+1278-245

Fixes #25285
Related: #10977, #22080.

The reqwest and reqwest-trait clients fill ResponseContent::entity by deserializing the body into the #[serde(untagged)] {Op}Error (and, with supportMultipleResponses, {Op}Success) enum. The status code is ignored. When several responses share a schema, the first variant that fits wins: a 409 returning Problem is reported as Status400, and a 201 returning Pet as Status200.

Changes

  • rust/reqwest/api.mustache and rust/reqwest-trait/api.mustache: when the enum has StatusXXX variants, the entity is picked by match status.as_u16():
    • an exact code deserializes into its variant;
    • a range code (4XX) matches 400..=499;
    • default matches _;
    • a response declared without a body becomes Some(StatusXXX()).
      If the body doesn't deserialize into the selected variant, or the status isn't declared, it falls back to UnknownValue(serde_json::Value), or None if the body isn't JSON. Responses are already sorted as exact codes, then ranges, then default, so the arms can't hide each other.
  • RustClientCodegen: sets x-rust-success-by-status / x-rust-error-by-status on operations that have 2xx/3xx or 4xx/5xx responses. Operations without them keep the existing serde_json::from_str(&content).ok() line, which avoids generating a single-arm match.
  • The public types are unchanged: the enums keep their variants and #[serde(untagged)].

Behavior change worth noting: for a declared status without a body (e.g. petstore's Status400()), entity is now Some(StatusXXX()) instead of None. This partially addresses #10977.

Not covered

  • hyper / hyper0x: they don't build a typed error entity, so they are untouched.
  • Deserializing the enums directly with serde is still shape-based.

Testing

  • New RustClientCodegenTest#testResponseEntityVariantIsSelectedByStatusCode (reqwest and reqwest-trait) with src/test/resources/3_0/rust/response-variant-by-status.yaml: exact, bodyless, range and default error responses, multiple 2xx with the same schema, exact (bodyless) and range 3xx responses, and an operation with only default.
  • cargo build --all-targets --all-features and cargo clippy in samples/client/petstore/rust and samples/client/others/rust: builds, and no new lints in the generated api files.
  • A crate generated from the test spec, called against a local stub server, returns Status409 for 409, Status404() for a bodyless 404, Status5XX for 503, DefaultResponse for 429, and Status201 / Status204() on success. The pre-fix output returned Status400 / Status200 for the same calls.

cc @frol @farcaller @richardwhiuk @paladinzh @jacob-pro @dsteeley

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
    Regenerated all rust client configs with ./bin/generate-samples.sh ./bin/configs/rust-reqwest-*.yaml ./bin/configs/rust-hyper-*.yaml ./bin/configs/rust-hyper0x-*.yaml. Only reqwest and reqwest-trait samples changed. No generator options changed, so no docs export was needed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.