Skip to content

Support renaming in zlink-macros derives - #289

Merged
zeenix merged 4 commits into
mainfrom
claude/github-issue-81-rename
Jul 16, 2026
Merged

Support renaming in zlink-macros derives#289
zeenix merged 4 commits into
mainfrom
claude/github-issue-81-rename

Conversation

@zeenix

@zeenix zeenix commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

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 to
    its variant names, and on a variant to that variant's fields. An explicit rename overrides it.
  • On CustomType, a container rename names the IDL type itself.
  • ReplyError variants can be renamed, changing the serialized error name.

The eight rename_all values 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 different
semantics for something Rust users already know.

So the workaround posted on the issue becomes:

#[derive(Debug, Serialize, Type)]
#[serde(rename_all = "camelCase")]
#[zlink(rename_all = "camelCase")]
struct Membership {
    user_name: String,
    group_name: String,
}

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 and ReplyError fields,
and it works on types that don't derive serde at all — the ReplyError derive writes its own serde
impls, 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

ReplyError variant renaming changes both derives in a single commit. The serde derive puts
the name on the wire, the introspection derive puts it in the IDL; both resolve through one shared
naming module, because a disagreement between them would be an interface description that lies
about the protocol.

A container rename is a hard error on the Type derive (its object type is anonymous) and on
both ReplyError derives (error names are qualified by #[zlink(interface)]). Silently ignoring
it is how people ship wrong IDL. This is technically breaking for anyone who wrote a rename that
never did anything.

Also here

The first commit is a standalone fix for a pre-existing bug: #[zlink(..)] parsing was
order-dependent in both directions — #[zlink(borrow, rename = "x")] silently dropped the rename,
and #[zlink(rename = "x", borrow)] silently dropped the borrow (losing zero-copy). The rename
work builds on these helpers and needs them to be order-independent.

Not included

Testing

cargo test --all-features: 458 passed, 0 failed (429 on main). The no_std path
(cargo test -p zlink-core --no-default-features --features idl-parse,proxy,defmt): 154 passed.
Clippy -D warnings and nightly fmt clean.

New tests cover all eight conventions for both fields and variants, rename overriding
rename_all, kebab-case names not breaking generated statics, the CustomType name and its
Type::Custom reference staying in sync, and — the important one — that the IDL error/field names
match what actually goes on the wire.

Generated by Claude Opus 4.8 (1M context).

zeenix added 3 commits July 16, 2026 19:39
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)
@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 4 untouched benchmarks


Comparing claude/github-issue-81-rename (03bfe7d) with main (0ab98f1)

Open in CodSpeed

Comment thread zlink-macros/src/lib.rs Outdated
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
zeenix force-pushed the claude/github-issue-81-rename branch from 069885d to 03bfe7d Compare July 16, 2026 21:37
@zeenix
zeenix enabled auto-merge July 16, 2026 21:38
@zeenix
zeenix merged commit a32c849 into main Jul 16, 2026
18 checks passed
@zeenix
zeenix deleted the claude/github-issue-81-rename branch July 16, 2026 21:43
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.

Support renaming in introspection derives for zlink-macros

1 participant