zyn-core 0.1.0

core ast and other type/helper implementations for zyn
Documentation

zyn

A template engine for Rust procedural macros. Write quote!-like code with control flow, interpolation pipes, and composable elements.

Install

[dependencies]
zyn = "0.0.0"

Quick Start

use zyn::prelude::*;

// Generate a greeting function with a configurable name
let name = quote::format_ident!("hello_world");
let is_pub = true;

let output: proc_macro2::TokenStream = zyn! {
    @if (is_pub) { pub }
    fn {{ name | snake }}() {
        println!("hello!");
    }
};

// output: pub fn hello_world() { println!("hello!"); }

Template Syntax

Interpolation

Insert any expression that implements ToTokens:

zyn! { fn {{ name }}() -> {{ ret_type }} {} }

Supports field access, method calls, and any Rust expression:

zyn! {
    {{ item.field.name }}: {{ item.field.ty }},
    {{ names.len() }}
}

Pipes

Transform interpolated values with pipes. Reference them in snake_case — they resolve to PascalCase structs automatically:

zyn! {
    fn {{ name | snake }}() {}      // HelloWorld -> hello_world
    const {{ name | screaming }}: &str = ""; // HelloWorld -> HELLO_WORLD
    {{ name | upper }}              // hello -> HELLO
    {{ name | lower }}              // HELLO -> hello
    {{ name | camel }}              // hello_world -> helloWorld
    {{ name | pascal }}             // hello_world -> HelloWorld
    {{ name | kebab }}              // HelloWorld -> "hello-world" (string literal)
}

Chain pipes:

zyn! { {{ name | snake | upper }} }  // HelloWorld -> HELLO_WORLD

Format pipes

Pipes can take arguments via : syntax. The ident and fmt pipes use a {} placeholder:

zyn! {
    fn {{ name | ident:"get_{}" }}() {}     // hello -> fn get_hello() {}
    fn {{ name | ident:"{}_impl" }}() {}    // hello -> fn hello_impl() {}
    const NAME: &str = {{ name | fmt:"{}" }};  // hello -> const NAME: &str = "hello";
}

Control Flow

Conditionals

zyn! {
    @if (is_async) {
        async fn {{ name }}() {}
    } @else if (is_unsafe) {
        unsafe fn {{ name }}() {}
    } @else {
        fn {{ name }}() {}
    }
}

Conditions support field access and method calls:

zyn! {
    @if (opts.is_pub) { pub }
    @if (items.is_empty()) { @throw "no items" }
}

Loops

zyn! {
    @for (name of ["x", "y", "z"].map(|s| quote::format_ident!("{}", s))) {
        pub {{ name }}: f64,
    }
}
// output: pub x: f64, pub y: f64, pub z: f64,

Pattern Matching

zyn! {
    @match (kind) {
        Kind::Struct => { struct {{ name }} {} }
        Kind::Enum => { enum {{ name }} {} }
        _ => {}
    }
}

Compile Errors

zyn! {
    @if (!valid) {
        @throw "expected a struct"
    }
}

Elements

Reusable template components. Define with #[element], invoke with @:

#[zyn::element]
fn field_decl(vis: syn::Visibility, name: syn::Ident, ty: syn::Type) -> syn::Result<proc_macro2::TokenStream> {
    Ok(zyn::zyn! {
        {{ vis }} {{ name }}: {{ ty }},
    })
}

// Generates struct FieldDecl, referenced as @field_decl in templates:
zyn! {
    @field_decl(
        vis = syn::parse_quote!(pub),
        name = quote::format_ident!("age"),
        ty = syn::parse_quote!(u32),
    )
}
// output: pub age: u32,

Elements can have zero parameters. Parentheses are optional when there are no props:

#[zyn::element]
fn divider() -> syn::Result<proc_macro2::TokenStream> {
    Ok(zyn::zyn!(const DIVIDER: &str = "---";))
}

// All equivalent:
zyn! { @divider }
zyn! { @divider() }

Elements support children via a children parameter:

#[zyn::element]
fn wrapper(vis: syn::Visibility, children: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
    Ok(quote::quote!(#vis struct Foo { #children }))
}

zyn! {
    @wrapper(vis = quote::quote!(pub)) {
        name: String,
        age: u32,
    }
}

Children-only elements can omit parens entirely:

#[zyn::element]
fn container(children: proc_macro2::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
    Ok(quote::quote!(mod inner { #children }))
}

zyn! {
    @container {
        struct Foo;
    }
}

Custom Pipes

Define with #[pipe]:

#[zyn::pipe]
fn prefix(input: String) -> proc_macro2::Ident {
    proc_macro2::Ident::new(
        &format!("pfx_{}", input),
        proc_macro2::Span::call_site(),
    )
}

// Generates struct Prefix, used as {{ name | prefix }}:
zyn! { {{ name | prefix }} }

Custom Names

Override the template name for elements and pipes:

#[zyn::element("my_component")]
fn internal_impl(...) -> ... { ... }

// Referenced as @my_component in templates (resolves to MyComponent struct)

Case Conversion Utilities

The case module and macros are available for use outside templates:

use zyn::case;

case::to_snake("HelloWorld")     // "hello_world"
case::to_pascal("hello_world")   // "HelloWorld"
case::to_camel("hello_world")    // "helloWorld"
case::to_screaming("HelloWorld") // "HELLO_WORLD"
case::to_kebab("HelloWorld")     // "hello-world"

// As macros (also work on syn::Ident):
zyn::pascal!("hello_world")           // "HelloWorld"
zyn::pascal!(ident => ident)          // syn::Ident in PascalCase
zyn::snake!(ident => ident)           // syn::Ident in snake_case

License

MIT