Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

zyn

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
let vis = if is_pub {
    quote!(pub)
} else {
    quote!()
};

let fields_ts: Vec<_> = fields
    .iter()
    .map(|f| {
        let name = &f.ident;
        let ty = &f.ty;
        quote! { #name: #ty, }
    })
    .collect();

quote! {
    #vis struct #ident {
        #(#fields_ts)*
    }
}
zyn! {
    @if (is_pub) { pub }
    struct {{ ident }} {
        @for (field in fields.iter()) {
            {{ field.ident }}: {{ field.ty }},
        }
    }
}

2. Case conversion needs external crates

Renaming an identifier means importing heck, calling a conversion function, then wrapping it in format_ident!.

❌ Before✅ After
use heck::ToSnakeCase;

let getter = format_ident!(
    "get_{}",
    name.to_string().to_snake_case()
);
{{ name | snake | ident:"get_{}" }}

3. Diagnostics are fragmented

Errors, warnings, notes, and help messages each use a different mechanism — or aren’t possible at all.

❌ Before✅ After
// errors
return syn::Error::new_spanned(
    &input,
    "expected a struct",
).to_compile_error().into();

// warnings — needs proc-macro-error crate
emit_warning!(span, "deprecated");

// notes/help — not possible on stable
#[zyn::element]
fn validated(
    #[zyn(input)] ident: syn::Ident,
) -> zyn::TokenStream {
    error!("expected a struct";
        span = ident.span());
    note!("only named structs supported");
    help!("change input to a struct");
    bail!();

    warn!("this derive is deprecated");
    zyn::zyn! { /* ... */ }
}

4. Attribute parsing is reinvented every time

Every project writes its own parser for #[my_attr(skip, rename = "foo")].

❌ Before✅ After
for attr in &input.attrs {
    if attr.path().is_ident("my_attr") {
        let args = attr.parse_args_with(
            Punctuated::<Meta, Token![,]>
                ::parse_terminated
        )?;
        for meta in &args {
            match meta {
                Meta::Path(p)
                    if p.is_ident("skip") => {}
                Meta::NameValue(nv)
                    if nv.path.is_ident("rename") => {}
                _ => {}
            }
        }
    }
}
use zyn::ext::{AttrExt, AttrsExt};

let args = input.attrs
    .find_args("my_attr")?;

if args.has("skip") { /* ... */ }

if let Some(rename) = args.get("rename") {
    /* ... */
}

5. Reusable codegen means manual helper functions

There’s no composition model — just functions returning TokenStream.

❌ Before✅ After
fn render_field(
    vis: &Visibility,
    name: &Ident,
    ty: &Type,
) -> TokenStream {
    quote! { #vis #name: #ty, }
}

let tokens: Vec<_> = fields
    .iter()
    .map(|f| render_field(
        &f.vis,
        f.ident.as_ref().unwrap(),
        &f.ty,
    ))
    .collect();

quote! { struct #ident { #(#tokens)* } }
#[zyn::element]
fn field_decl(
    vis: syn::Visibility,
    name: syn::Ident,
    ty: syn::Type,
) -> zyn::TokenStream {
    zyn::zyn! { {{ vis }} {{ name }}: {{ ty }}, }
}

zyn! {
    struct {{ ident }} {
        @for (field in fields.iter()) {
            @field_decl(
                vis = field.vis.clone(),
                name = field.ident.clone().unwrap(),
                ty = field.ty.clone(),
            )
        }
    }
}

6. Five crates doing five things

❌ Before✅ After
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
heck = "0.5"
proc-macro-error = "1"
[dependencies]
zyn = "0.3"

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 }}
        }
    }
}
ConcernZyn approach
Code generationzyn! template with {{ }} interpolation — reads like the code it generates
Control flow@if, @for, @match directives inline — no .iter().map().collect()
Case conversionBuilt-in pipes: {{ name | snake }}, {{ name | pascal }}, {{ name | screaming }} — no extra crate
Name formatting{{ name | ident:"get_{}" }} — one expression, no let binding
Diagnosticserror!, 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
Debuggingzyn::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 any ToTokens value, with field access and method calls
  • Pipes{{ name | snake }}, {{ name | ident:"get_{}" }}, {{ name | str }} — 13 built-in pipes plus custom
  • Control flow@if, @for, @match with full nesting
  • Diagnosticserror!, 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
  • Debuggingzyn::debug! with pretty, raw, and ast modes
  • Attribute parsing#[derive(Attribute)] for typed attribute structs
  • Case conversionsnake, camel, pascal, screaming, kebab, upper, lower, trim, plural, singular