Fix attribute-metadata parsing and serde rename-rule parity (for rpc-toolkit TS bindings)#1
Conversation
- Parse attribute lists as Punctuated<Meta> so multi-item attributes (#[serde(tag="t", content="c")], rename + other items, split-form rename(serialize=..., deserialize=...)) survive as structured AttributeMeta instead of collapsing to Unparsed. - Port serde's RenameRule with distinct apply_to_variant / apply_to_field algorithms (fixes acronym / consecutive-capital divergence and the variant-vs-field asymmetry; PascalCase-on-variant is identity, etc.). - Honor serde skip / skip_serializing (alongside #[visit(skip)]) when filtering struct fields, and don't impose a Sync bound for skipped fields. - Dedupe the duplicate rename helper; add metadata + acronym tests.
…luated) The derive emitted `#[cfg(feature = "meta")]` and `cfg!(feature = "meta")` into consumer code, but those cfgs evaluate against the *consumer* crate's features, not visit-rs's. A downstream crate without its own `meta` feature got a StructInfoData with the `metadata` field cfg'd out (E0063) and empty metadata arrays — making attribute metadata unusable cross-crate. Make the `metadata` fields and the `metadata` module unconditional, and always emit the metadata arrays from the derive. `meta` stays as a no-op feature for back-compat.
|
Companion PR on the rpc-toolkit side (the actual auto-generated TS-bindings work, which depends on this branch): Start9Labs/rpc-toolkit#4 That branch points |
Makes the `ts` feature compile and produce serde-accurate TypeScript.
- Drop the runtime-`syn` SerdeTag hack; resolve the serde enum
representation (external/internal/adjacent/untagged) from visit-rs's
runtime AttributeMeta, and emit correct TS for every variant kind:
external `{"V":payload}` / "V" for unit, internal `{tag:"V"}&payload`,
adjacent `{tag:"V";content:payload}`, untagged bare payload.
- Add impl_ts_enum! so enums can opt in (parallels impl_ts_struct!).
- impl TS for Option<T> (T|null); honor serde skip/skip_serializing
(omit) and skip_serializing_if/default (optional `?:`) via field metadata.
- Map all integer widths to `number` (serde_json emits JSON numbers, not
bigint) and map keys to `{[key:string]:V}` (JSON keys are always strings).
- no_ts() children emit a well-formed `{_PARAMS:unknown;_RETURN:unknown}`
leaf instead of bare `unknown` (which poisoned the whole tree's inference).
- insert_definition no longer panics in release on name clashes.
- Gate the impl_ts_struct! use behind cfg(ts) so --no-default-features builds.
- Add TSVisitor::into_module + handler_bindings() to emit a complete .d.ts
module, plus examples/generate_ts.rs.
- Depend on the fixed visit-rs branch (features=["meta"]); revert to a
published release once dr-bonez/visit-rs#1 merges.
- Tests assert exact TS for structs, all four enum taggings, Option, maps,
named definitions, and a handler tree; verified end-to-end with tsc --strict.
Wrap the generated enum impls in an anonymous `const _: () = { use ... as _; ... }`
block so their method bodies can use trait-method call syntax without the caller
importing Visit/EnumInfo (matching the struct derive's ergonomics).
|
Pushed an import-free improvement to the Note on generic/lifetime support: I scoped this out and it's a larger derive-wide change than a metadata fix — lifetime params don't compile for structs either, and the enum methods need hygienic lifetimes + per-generic-param bounds (incl. |
The 12 generated Visit* enum impls now compile for enums with generic type parameters (and lifetime parameters), where the parameters are 'static — which covers RPC types (owned, deserialized). - Emit impl-generics via a helper that injects `__visit_rs__V` in the correct position (was `impl<__visit_rs__V, #impl_generics>`, which produced `impl<__visit_rs__V, <T>>` for generic enums); splice the enum's own where-clause to avoid a duplicate `where`. - Use a hygienic `'__visit_rs__a` method lifetime (was `'a`, which shadowed an enum's own `'a`). - Drop the explicit `+ '__visit_rs__a` from the RPIT of `&self` iterator methods (mirrors the struct derive; the explicit bound forced `T: '__a`). - Add `#ty: Sync` to the `&self` async methods (their futures capture `&self`) and the trait's method-level `where V: Send` bounds. - Add `Self: 'static` to the visit impls — required because `for<'a> Variant<'a, Self>: Visit<V>` borrows Self under an HRTB. Non-generic enums satisfy it trivially; generic enums require their parameters to be 'static. - Add generic/bounded enum tests.
|
Pushed generic + lifetime enum support for What it took (the derive now mirrors the struct derive's machinery):
New tests in Caveat (documented): generic params must be |
a6a1120 to
8fb09da
Compare
Foundational fixes in
visit-rs-deriveneeded by rpc-toolkit'sfeature/new-ts(auto-generated TS bindings). These came out of an adversarial review of both repos; this PR addresses the must-fix metadata/rename findings on the visit-rs side.What was broken
Multi-item attributes collapsed to
Unparsed.parse_meta_to_attribute_meta(and all four rename helpers) parsed an attribute list's tokens withsyn::parse2::<Meta>— a singleMeta. Any comma-separated attribute failed that parse:#[serde(tag = "t", content = "c")](adjacently-tagged enums) →AttributeMeta::Unparsed, so tag/content were unreadable downstream.#[serde(rename = "x", default)],#[serde(rename_all = "snake_case", deny_unknown_fields)], etc. → rename/rename_all silently dropped, falling back to the Rust identifier.#[serde(rename(serialize = "s", deserialize = "d"))]was never recognized.Rename rules diverged from serde. The hand-rolled case converters used one algorithm for both fields and variants and mishandled acronyms / consecutive capitals (
IOError→ioerrorinstead of serde'si_o_error). serde actually uses two distinct algorithms (apply_to_variantvsapply_to_field; e.g. snake_case is identity for fields, PascalCase is identity for variants).serde
skipignored. Only#[visit(skip)]was honored;#[serde(skip)]/#[serde(skip_serializing)]fields were still emitted. Also, theSyncwhere-bound was imposed even for#[visit(skip)]-ed fields.Changes
Punctuated::<Meta, Token![,]>::parse_terminated, recursively — multi-item and nested/split-form attributes now survive as structuredAttributeMeta::List { items }.RenameRule(apply_to_variant+apply_to_field).renamefrom bothrename = "..."andrename(serialize=..., deserialize=...)(serialize wins, matching the serialized wire shape).#[serde(skip)]/skip_serializinginfield_iter, and only bound non-skipped fields withSync.get_rename_attributeinlib.rs.visit-rs0.1.9 → 0.1.10,visit-rs-derive0.1.7 → 0.1.8.Tests
test_case_conversionsupdated: PascalCase-on-variant is identity per serde (was asserting the old non-serdeTestVariant); verified againstserde_json.enum_rename::test_acronym_variant_conversions(IOError→i_o_error,i-o-error, …).tests/metadata.rs: adjacently-taggedtag+contentsurvive as structured metadata (notUnparsed), and arenamecombined with another list item is honored.cargo test --allis green.Notes / deferred (found in review, not in this PR)
VisitVariantsimpls spliceImplGenericsinsideimpl<__visit_rs__V, …>). Orthogonal to TS-binding metadata; happy to do as a follow-up.#[visit(skip)]/ serdeskipon enum-variant fields isn't filtered inenum_variants.rs(struct fields are). rpc-toolkit handles this consumer-side via field metadata for now.cc @dr-bonez — opening from a fork since I only have read access here. rpc-toolkit's companion PR (the actual TS-binding work) depends on this branch and will be linked.