Introduction
Zyn is a template engine and framework for Rust procedural macros. One crate replaces the patchwork of syn, quote, case-conversion libraries, diagnostic helpers, and attribute-parsing boilerplate that every proc macro project ends up assembling from scratch.
zyn! {
@for (field in fields.iter()) {
pub fn {{ field.ident | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
Why?
The problem
Building a proc macro in Rust today means pulling in a handful of loosely related crates and gluing them together yourself. Here’s what each pain point looks like — and how zyn solves it.
1. quote! has no control flow
Every conditional or loop forces you out of the template and back into Rust.
| ❌ Before | ✅ After |
|---|---|
|
|
2. Case conversion needs external crates
Renaming an identifier means importing heck, calling a conversion function, then wrapping it in format_ident!.
| ❌ Before | ✅ After |
|---|---|
|
|
3. Diagnostics are fragmented
Errors, warnings, notes, and help messages each use a different mechanism — or aren’t possible at all.
| ❌ Before | ✅ After |
|---|---|
|
|
4. Attribute parsing is reinvented every time
Every project writes its own parser for #[my_attr(skip, rename = "foo")].
| ❌ Before | ✅ After |
|---|---|
|
|
5. Reusable codegen means manual helper functions
There’s no composition model — just functions returning TokenStream.
| ❌ Before | ✅ After |
|---|---|
|
|
6. Five crates doing five things
| ❌ Before | ✅ After |
|---|---|
|
|
What zyn does differently
Zyn replaces the entire stack with a single zyn! template macro and a set of companion tools:
zyn! {
@for (field in fields.iter()) {
pub fn {{ field.ident | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
| Concern | Zyn approach |
|---|---|
| Code generation | zyn! template with {{ }} interpolation — reads like the code it generates |
| Control flow | @if, @for, @match directives inline — no .iter().map().collect() |
| Case conversion | Built-in pipes: {{ name | snake }}, {{ name | pascal }}, {{ name | screaming }} — no extra crate |
| Name formatting | {{ name | ident:"get_{}" }} — one expression, no let binding |
| Diagnostics | error!, warn!, note!, help!, bail! macros in #[zyn::element] bodies — one API for all diagnostic levels |
| Attribute parsing | #[derive(Attribute)] for typed attribute structs — built-in, no darling dependency |
| Reusable codegen | #[zyn::element] — composable template components invoked with @name(props) |
| Value transforms | #[zyn::pipe] — custom pipes that chain with built-ins |
| Proc macro entry points | #[zyn::derive] and #[zyn::attribute] — replace #[proc_macro_derive]/#[proc_macro_attribute] with auto-parsed Input and diagnostics |
| Debugging | zyn::debug! — drop-in zyn! replacement that prints the expansion (pretty, raw, ast modes) |
| String output | {{ name | str }} — stringify to a LitStr without ceremony |
One dependency. No runtime cost. Everything expands at compile time into the same TokenStream-building code you’d write by hand — just without the boilerplate.
Features
- Interpolation —
{{ expr }}inserts anyToTokensvalue, with field access and method calls - Pipes —
{{ name | snake }},{{ name | ident:"get_{}" }},{{ name | str }}— 13 built-in pipes plus custom - Control flow —
@if,@for,@matchwith full nesting - Diagnostics —
error!,warn!,note!,help!,bail!macros in#[zyn::element]bodies - Elements — reusable template components via
#[zyn::element] - Custom pipes — define transforms with
#[zyn::pipe] - Proc macro entry points —
#[zyn::derive]and#[zyn::attribute]with auto-parsed input, extractors, and diagnostics - Debugging —
zyn::debug!withpretty,raw, andastmodes - Attribute parsing —
#[derive(Attribute)]for typed attribute structs - Case conversion —
snake,camel,pascal,screaming,kebab,upper,lower,trim,plural,singular