Zyn
A procedural macro development framework designed to simplify and add structure to rust macro development
Install / Use
/learn @aacebo/ZynREADME
A proc macro framework with templates, composable elements, and built-in diagnostics.
cargo add zyn
Table of Contents
Templates
Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

The zyn! macro is the core of zyn. Write token output as if it were source code,
with {{ }} interpolation and @ control flow directives.
Interpolation — any ToTokens value:
let name = zyn::format_ident!("hello_world");
zyn::zyn!(fn {{ name }}() {})
// → fn hello_world() {}
Pipes — transform values inline:
zyn::zyn!(fn {{ name | pascal }}() {})
// name = "hello_world" → fn HelloWorld() {}
Control flow:
zyn::zyn!(
@if (is_pub) { pub }
@for (field in fields.named.iter()) {
fn {{ field.ident }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
)
Full template syntax:
| Syntax | Purpose |
|--------|---------|
| {{ expr }} | Interpolate any ToTokens value |
| {{ expr \| pipe }} | Transform value through a pipe before inserting |
| @if (cond) { ... } | Conditional token emission |
| @else { ... } | Else branch |
| @else if (cond) { ... } | Else-if branch |
| @for (x in iter) { ... } | Loop over an iterator |
| @for (N) { ... } | Repeat N times |
| @match (expr) { pat => { ... } } | Pattern-based emission |
| @element_name(prop = val) | Invoke a #[element] component |
Elements
Elements are reusable template components defined with #[zyn::element].
They encapsulate a fragment of token output and accept typed props.
Define an element:
#[zyn::element]
fn getter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn! {
pub fn {{ name | snake | ident:"get_{}" }}(&self) -> &{{ ty }} {
&self.{{ name }}
}
}
}
Invoke it inside any template with @:
zyn::zyn! {
impl {{ ident }} {
@for (field in fields.named.iter()) {
@getter(name = field.ident.clone().unwrap(), ty = field.ty.clone())
}
}
}
Elements can also receive extractors — values resolved automatically from proc macro
input — by marking a param with #[zyn(input)]:
#[zyn::derive]
fn my_getters(
#[zyn(input)] ident: zyn::Extract<zyn::syn::Ident>,
#[zyn(input)] fields: zyn::Fields<zyn::syn::FieldsNamed>,
) -> zyn::TokenStream {
zyn::zyn! {
impl {{ ident }} {
@for (field in fields.named.iter()) {
pub fn {{ field.ident | snake | ident:"get_{}" }}(&self) -> &{{ field.ty }} {
&self.{{ field.ident }}
}
}
}
}
}
// Applied to: struct User { first_name: String, age: u32 }
// Generates:
// impl User {
// pub fn get_first_name(&self) -> &String { &self.first_name }
// pub fn get_age(&self) -> &u32 { &self.age }
// }
Pipes
Pipes transform interpolated values: {{ expr | pipe }}. They chain left to right:
zyn::zyn!(fn {{ name | snake | ident:"get_{}" }}() {})
// name = "HelloWorld" → fn get_hello_world() {}
Built-in pipes:
| Pipe | Input example | Output |
|------|--------------|--------|
| snake | HelloWorld | hello_world |
| pascal | hello_world | HelloWorld |
| camel | hello_world | helloWorld |
| screaming | HelloWorld | HELLO_WORLD |
| kebab | HelloWorld | "hello-world" |
| upper | hello | HELLO |
| lower | HELLO | hello |
| str | hello | "hello" |
| trim | __foo__ | foo |
| plural | user | users |
| singular | users | user |
| ident:"pattern_{}" | hello | pattern_hello (ident) |
| fmt:"pattern_{}" | hello | "pattern_hello" (string) |
Custom pipes via #[zyn::pipe]:
#[zyn::pipe]
fn shout(input: String) -> zyn::syn::Ident {
zyn::syn::Ident::new(&format!("{}_BANG", input.to_uppercase()), zyn::Span::call_site())
}
zyn::zyn!(fn {{ name | shout }}() {})
// name = "hello" → fn HELLO_BANG() {}
Attributes
zyn provides two tools for attribute handling: a derive macro for typed parsing and a proc macro attribute for writing attribute macros.
Typed attribute structs via #[derive(Attribute)]:
#[derive(zyn::Attribute)]
#[zyn("builder")]
struct BuilderConfig {
#[zyn(default)]
skip: bool,
#[zyn(default = "build".to_string())]
method: String,
}
// users write: #[builder(skip)] or #[builder(method = "create")]
The derive generates from_args, FromArg, and FromInput implementations, as well as
a human-readable about() string for error messages.
Auto-suggest
When a user misspells an argument name, zyn automatically suggests the closest known field. No extra setup required:
error: unknown argument `skiip`
--> src/main.rs:5:12
|
5 | #[builder(skiip)]
| ^^^^^
|
= help: did you mean `skip`?
Suggestions are offered when the edit distance is ≤ 3 characters. Distant or completely unknown keys produce only the "unknown argument" error without a suggestion.
Attribute proc macros via #[zyn::attribute]:
#[zyn::attribute]
fn my_attr(#[zyn(input)] item: zyn::syn::ItemFn, args: zyn::Args) -> zyn::TokenStream {
// args: parsed key=value arguments from the attribute invocation
zyn::zyn!({ { item } })
}
Testing
Assertions
zyn! returns Output — test both tokens and diagnostics directly:
#[test]
fn generates_function() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput).unwrap().into();
let output = zyn::zyn!(fn hello() {});
let expected = zyn::quote::quote!(fn hello() {});
zyn::assert_tokens!(output, expected);
}
Diagnostic assertions check error messages from error!, warn!, bail!:
#[zyn::element]
fn validated(name: zyn::syn::Ident) -> zyn::TokenStream {
if name == "forbidden" {
bail!("reserved identifier `{}`", name);
}
zyn::zyn!(fn {{ name }}() {})
}
#[test]
fn rejects_forbidden_name() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput).unwrap().into();
let output = zyn::zyn!(@validated(name = zyn::format_ident!("forbidden")));
zyn::assert_diagnostic_error!(output, "reserved identifier");
zyn::assert_tokens_empty!(output);
// ✓ error diagnostic present, no tokens produced
}
#[test]
fn accepts_valid_name() {
let input: zyn::Input = zyn::parse!("struct Foo;" => zyn::syn::DeriveInput).unwrap().into();
let output = zyn::zyn!(@validated(name = zyn::format_ident!("hello")));
zyn::assert_tokens_contain!(output, "fn hello");
// ✓ tokens contain "fn hello"
}
| Macro | Purpose |
|-------|---------|
| assert_tokens! | Compare two token streams |
| assert_tokens_empty! | Assert no tokens produced |
| assert_tokens_contain! | Check for substring in output |
| assert_diagnostic_error! | Assert error diagnostic with message |
| assert_diagnostic_warning! | Assert warning diagnostic |
| assert_diagnostic_note! | Assert note diagnostic |
| assert_diagnostic_help! | Assert help diagnostic |
| assert_compile_error! | Alias for assert_diagnostic_error! |
With the pretty feature:
| Macro | Purpose |
|-------|---------|
| assert_tokens_pretty! | Compare using prettyplease-formatted output |
| assert_tokens_contain_pretty! | Substring check on pretty-printed output |
Debugging
Inspect generated code by adding debug to any zyn attribute macro. Set ZYN_DEBUG to the generated type name (or * for all) to enable output:
#[zyn::element(debug)]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
ZYN_DEBUG="*" cargo build
note: zyn::element ─── Greeting
fn {{ name }}() {}
--> src/lib.rs:1:1
Without injection, props show as {{ name }} placeholders. With the pretty feature, use debug(pretty) for formatted output:
#[zyn::element(debug(pretty))]
fn greeting(name: zyn::syn::Ident) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}() {})
}
note: zyn::element ─── Greeting
fn {{ name }}() {}
--> src/lib.rs:1:1
Use debug(full) to emit the full generated struct + impl instead of just the body. Combine with pretty: debug(pretty, full).
Supply key = "value" injection pairs to substitute static values for props — useful when the real value isn't known at proc-macro time:
#[zyn::element(debug(name = "Foo", ty = "String"))]
fn setter(name: zyn::syn::Ident, ty: zyn::syn::Type) -> zyn::TokenStream {
zyn::zyn!(fn {{ name }}(value: {{ ty }}) -> Self { self })
}
Output: fn Foo(value : String) -> Self { self }. Without injection, props show as {{ name }}, {{ ty }}.
!
