Skip to content

API ergonomics & consistency pass (pre-tag)#46

Open
zheylmun wants to merge 18 commits into
mainfrom
refactor/flatten-suppressable-requests
Open

API ergonomics & consistency pass (pre-tag)#46
zheylmun wants to merge 18 commits into
mainfrom
refactor/flatten-suppressable-requests

Conversation

@zheylmun

@zheylmun zheylmun commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

A pre-tag consistency/ergonomics sweep of the public API. No behavioral change for conformant wire traffic — this pass touches derives, associated functions, two new named response wrappers, and one accessor rename. Encode/decode of valid frames is byte-for-byte identical.

Spec & plan: docs/superpowers/specs/2026-07-01-api-consistency-pass-design.md, docs/superpowers/plans/2026-07-01-api-consistency-pass.md.

What changed

Tier 1 — uniform derives on the zero-copy borrowed types. The older POD types already had the full derive treatment; the borrowed (&'a [u8]-backed) types did not. Now every *Request/*Response carries serde + utoipa cfg-derives, Eq, and #[non_exhaustive] (with #[serde(borrow)] on borrowed fields). ReadDTCInfoRequest::new is now const. Also added Eq to DTCFormatIdentifier (a prerequisite that unblocked ReadDTCInfoResponse: Eq).

Tier 2 — allowed_nack_codes() completed. Added to the 6 services that lacked it, so all 15/15 services now expose fn() -> &'static [NegativeResponseCode] uniformly.

Tier 3a — response symmetry. Introduced named ClearDiagnosticInfoResponse and ReadDataByIdentifierResponse<'a> types, replacing the bare Response::ClearDiagnosticInfo unit variant and the inline Response::ReadDataByIdentifier(&[u8]). Every Response variant now carries a named, exported, serde-able type.

Tier 3b — accessor convention. Standardized scalar-newtype accessors on const fn value() (renamed the lone outlier SecurityAccessLevel::get()).

Intentional deviations (reviewed & documented)

  • ReadDataByIdentifierRequest did not get serde/utoipa. Its private Dids::Native(&[u16]) backing has no borrowed-Deserialize impl in serde (only &[u8]/&str support zero-copy borrow). Carved out and documented inline. The response (wrapping &[u8]) got full serde.
  • ClearDiagnosticInfo decode is now strict. A positive response with trailing bytes after the SID is now rejected (via decode_exact), matching every other response arm. Conformant traffic ([0x54]) is unaffected. A test asserts both paths.

Verification

  • cargo test green across default, --no-default-features, --no-default-features --features alloc, --all-features (163 unit + 3 doctests).
  • cargo clippy --all-features -- -D warnings and --no-default-features clean.
  • cargo fmt --check clean; cargo doc --all-features clean (no missing_docs).
  • MSRV 1.85.0 and no_std build preserved.
  • ⚠️ Fuzz smoke (fuzz_roundtrip/fuzz_request_decode/fuzz_response_decode) not runcargo-fuzz isn't installed locally. Worth running before the tag since the Response enum changed shape.

Process

Executed task-by-task with a fresh implementer + independent spec/quality review per task, plus a final whole-branch review (no Critical findings). Every derive addition is guarded by a compile-time trait-bound assertion.

🤖 Generated with Claude Code

zheylmun added 17 commits July 1, 2026 15:47
…toipa carve-out

Adds #[non_exhaustive] to ReadDataByIdentifierRequest and a derive_contract
test (Eq assertion only). serde omitted: &[u16] has no borrowed Deserialize
impl in serde. utoipa omitted: requires Dids (pub(crate)) to impl ToSchema.
Both carve-outs documented inline matching ReadDTCInfoResponse precedent.
- DTCFormatIdentifier: add Eq (prerequisite for ReadDTCInfoResponse Eq)
- ReadDTCInfoRequest: add Eq; make new() const
- DTCFaultDetectionCounterRecord: add Eq
- ReadDTCInfoResponse: add Eq, PartialEq, and serde/utoipa cfg-derives
  (serde(borrow) on each &'a [u8] field); all contained types had serde
@zheylmun zheylmun requested a review from JustinKovacich July 1, 2026 20:44
CI runs `clippy --all-targets --all-features -- -D warnings -D clippy::pedantic`,
which denies pedantic lints in test code. The new response round-trip tests bound
`(resp, rest)` / `(decoded, rest)`, tripping clippy::similar_names. Rename the slice
binding to `remaining` (the existing convention in these files).
@zheylmun zheylmun force-pushed the refactor/flatten-suppressable-requests branch from 59d5c53 to b9013ce Compare July 1, 2026 20:45

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

Nice, tightly-scoped consistency pass — I built the branch and the suite is green (163 + 3 doctests), no correctness or cross-file breakage, and encode output is byte-for-byte unchanged for conformant frames. The comments below are all about the newly-introduced serde story. Findings 1 and 3 are inline; finding 2 can't be a line comment because it's in Cargo.toml (outside this PR's diff), so noting it here:

serde_bytes is a declared dependency, force-enabled by the serde feature, but never used. Cargo.toml:25 (serde = ["dep:serde", "dep:serde_bytes"]) and :36 pull it in, but grep serde_bytes src/ and grep 'serde(with' src/ both return nothing. It's dead weight that also inflates the serde feature's build — and per the inline comment on read_data_by_identifier.rs, wiring it up wouldn't fix the JSON round-trip anyway (I tested it). Suggest either removing the dep or reconsidering the byte-field serde strategy it was presumably added for.

#[non_exhaustive]
pub struct ReadDataByIdentifierResponse<'a> {
#[cfg_attr(feature = "serde", serde(borrow))]
records: &'a [u8],

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.

Borrowed &[u8] types advertise Deserialize but can't be deserialized from JSON (applies to every #[serde(borrow)] &[u8] field added here: this one, transfer_data.rs req/resp data, security_access.rs request_data/security_seed, routine_control.rs option_record/status_record, request_download.rs max_number_of_block_length, write_data_by_identifier.rs data, and the read_dtc_information.rs raw_records variants).

Reproduced in a standalone crate: serde_json::to_string of ReadDataByIdentifierResponse emits {"records":[1,2,255]}, but serde_json::from_str of that exact output fails at runtime:

invalid type: sequence, expected a borrowed byte array

It compiles, so CI stays green — but the type publicly implements Deserialize, and this PR pairs these types with utoipa::ToSchema (an OpenAPI/JSON/HTTP story), so a client generated from the schema sends JSON the Rust side then can't read back.

Heads-up on the obvious fix: #[serde(with = "serde_bytes")] does not fix this — I tested it, and borrowed &[u8] still can't deserialize from a JSON array (inherent to zero-copy + self-describing text formats). It only round-trips in borrowing binary formats (bincode/postcard), where plain #[serde(borrow)] already works. Real options: (a) document these as binary-format-only and drop the JSON/utoipa expectation for byte fields; (b) deserialize into Cow<'a, [u8]>/Vec<u8>; or (c) add a custom deserializer that also accepts an integer array.

Comment thread src/test_util.rs
/// Compile-time assertion that `T` round-trips serde (borrowed deserialize allowed).
#[cfg(feature = "serde")]
#[allow(dead_code)]
pub(crate) const fn assert_impl_serde<'de, T: serde::Serialize + serde::Deserialize<'de>>() {}

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.

This assertion gives false confidence for the borrowed-serde types. assert_impl_serde::<T<'static>>() only checks that the Serialize + Deserialize<'de> bounds exist; it never instantiates a deserializer or performs a round-trip. That's exactly why the whole suite is green while JSON deserialize of the &[u8] types is broken (see the comment on read_data_by_identifier.rs). Consider a real round-trip assertion instead — e.g. a bincode serialize→deserialize round-trip, or a serde_json test that documents the expected failure — so the true capability is explicit rather than implied by a bound check.

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.

2 participants