SkillAgentSearch skills...

Zyn

A procedural macro development framework designed to simplify and add structure to rust macro development

Install / Use

/learn @aacebo/Zyn
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<img src="https://raw.githubusercontent.com/aacebo/zyn/refs/heads/main/assets/banner.svg" style="border-radius:10px">

Getting Started docs.rs crates.io bencher.dev Crates.io Size

A proc macro framework with templates, composable elements, and built-in diagnostics.

🗺️ Roadmap

cargo add zyn

Table of Contents


Templates

Templates are fully type-checked at compile time — errors appear inline, just like regular Rust code.

Compile-time type safety

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 }}.

!

View on GitHub
GitHub Stars165
CategoryDevelopment
Updated7d ago
Forks5

Languages

Rust

Security Score

95/100

Audited on Apr 3, 2026

No findings