Support renaming in zlink-macros derives - #289
Merged
Merged
Conversation
Walking `#[zlink(..)]` nested meta assumed every key had a value to consume, so a valueless key like `borrow` aborted the walk once it reached a later key. `parse_crate_path` and the rename-attribute parser both had this shape: `#[zlink(borrow, rename = "x")]` silently dropped the rename because the walk died on `borrow` before ever reaching it. `has_zlink_bool_attr` had the mirror-image bug: its closure never consumed a *valued* key's payload, so the walk choked on `=` where syn expected a `,` before the next key. `#[zlink(rename = "x", borrow)]` silently dropped the `borrow` because the walk died on `rename = "x"` first. Both orderings were broken, just in different halves of the parsing code, so whether a `#[zlink(..)]` attribute worked depended entirely on which key the user happened to write first. Peek before consuming instead, so bare, name-value and list-shaped keys all skip cleanly regardless of order. Renaming support lands on these helpers next and needs them to be order-independent. Assisted-by: Claude Opus 4.8 (1M context)
IDL names came straight from Rust idents, so a type whose wire format differs from its Rust spelling could not be described at all. The only way out was hand-writing `introspect::Type`, which is what users on #81 resorted to. Name resolution is centralised in a new `naming` module because the IDL derives and the `ReplyError` derive (renamed variants coming next) describe the same names -- if they resolved names differently the IDL would misdescribe the wire. The case conventions and their spellings are serde's, deliberately: the attributes are zlink's own, but there's no value in inventing different semantics for something Rust users already know. `rename_all` on a struct applies to its fields, on an enum to its variant names; `rename` on an item overrides it. On `CustomType` a container `rename` names the IDL type itself, and the generated `Type::Custom` reference follows it. On the `Type` derive a container rename is an error rather than a no-op: the object type it generates is anonymous, so accepting the attribute would only promise something we cannot deliver. Field statics stay keyed on the Rust ident -- `kebab-case` names are not valid identifiers. Assisted-by: Claude Opus 4.8 (1M context)
An error's wire name was always `{interface}.{VariantIdent}`, so an
interface whose error names differ from Rust's spelling was out of reach.
Both derives change together and resolve names through the same code: the
serde derive puts the name on the wire, the introspection derive puts it
in the IDL, and a disagreement between them would be a description that
lies about the protocol. `rename_all` on the enum applies to variant
names, on a variant to that variant's fields.
Note the `Deserialize` helper enum now has our attributes stripped from
its variants, not just its fields -- it only derives `Deserialize`, so a
leaked `#[zlink(..)]` would not resolve there.
Assisted-by: Claude Opus 4.8 (1M context)
zeenix
commented
Jul 16, 2026
Spell out that renaming the IDL does not rename the wire format. Since we deliberately do not read serde's attributes, a type deriving both must state the renaming twice, and nothing detects a disagreement -- so the docs need to say it plainly rather than leave it to be discovered. The compile-fail examples pin the two cases where a container `rename` is rejected instead of silently ignored. Assisted-by: Claude Opus 4.8 (1M context)
zeenix
force-pushed
the
claude/github-issue-81-rename
branch
from
July 16, 2026 21:37
069885d to
03bfe7d
Compare
zeenix
enabled auto-merge
July 16, 2026 21:38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #81.
Renaming is now supported in the derives, driven by zlink's own attributes:
#[zlink(rename = "...")]on a field or variant sets its name.#[zlink(rename_all = "...")]on a struct applies a case convention to its fields, on an enum toits variant names, and on a variant to that variant's fields. An explicit
renameoverrides it.CustomType, a containerrenamenames the IDL type itself.ReplyErrorvariants can be renamed, changing the serialized error name.The eight
rename_allvalues and their semantics are serde's (lowercase,UPPERCASE,PascalCase,camelCase,snake_case,SCREAMING_SNAKE_CASE,kebab-case,SCREAMING-KEBAB-CASE) — the attributes are ours, but there's no value in inventing differentsemantics for something Rust users already know.
So the workaround posted on the issue becomes:
On serde's attributes
The issue asked to investigate reusing serde's attributes. That was investigated and rejected:
it's consistent with the existing
#[zlink(rename)]API on proxy methods andReplyErrorfields,and it works on types that don't derive serde at all — the
ReplyErrorderive writes its own serdeimpls, so
#[serde(..)]wouldn't even compile there.The trade-off is real and accepted: a type deriving both serde's and zlink's traits must state the
renaming twice, and nothing detects a disagreement. The derive docs say so plainly rather than
leave it to be discovered.
Two design points worth review
ReplyErrorvariant renaming changes both derives in a single commit. The serde derive putsthe name on the wire, the introspection derive puts it in the IDL; both resolve through one shared
namingmodule, because a disagreement between them would be an interface description that liesabout the protocol.
A container
renameis a hard error on theTypederive (its object type is anonymous) and onboth
ReplyErrorderives (error names are qualified by#[zlink(interface)]). Silently ignoringit is how people ship wrong IDL. This is technically breaking for anyone who wrote a
renamethatnever did anything.
Also here
The first commit is a standalone fix for a pre-existing bug:
#[zlink(..)]parsing wasorder-dependent in both directions —
#[zlink(borrow, rename = "x")]silently dropped the rename,and
#[zlink(rename = "x", borrow)]silently dropped theborrow(losing zero-copy). The renamework builds on these helpers and needs them to be order-independent.
Not included
#[serde(skip)]/flatten/tagequivalents. These also affect what's on the wire and so canproduce wrong IDL, but they aren't renaming. Introspection derives describe fields serde doesn't send: no equivalent for
skip,flatten,tag#291r#typefield fails to compile in the introspectionderives and reaches the wire as the
r#typekey. Pre-existing, and fixing it would change anexisting derive's wire format. Raw identifiers are mishandled by the derives: panic in introspection,
r#leaks to the wire #290Testing
cargo test --all-features: 458 passed, 0 failed (429 onmain). The no_std path(
cargo test -p zlink-core --no-default-features --features idl-parse,proxy,defmt): 154 passed.Clippy
-D warningsand nightly fmt clean.New tests cover all eight conventions for both fields and variants,
renameoverridingrename_all, kebab-case names not breaking generated statics, theCustomTypename and itsType::Customreference staying in sync, and — the important one — that the IDL error/field namesmatch what actually goes on the wire.
Generated by Claude Opus 4.8 (1M context).