macro-template 0.1.0-rc.5

Macro to generate repeated Rust code with table-driven inputs.
Documentation

macro-template

Crates.io Documentation MSRV 1.85 Apache 2.0 licensed Build Status

macro-template is a procedural macro that generates repeated Rust code in multiple places with table-driven inputs.

Motivation

This crate is inspired by match-template and macro_find_and_replace.

When developing ScopeDB, we introduced these two proc-macros to generate repeated code for match arms and impls. I noticed that they share a common pattern: iterating over a table of values and generating code based on it. I wanted to unify these patterns into a single, concise, but flexible macro that can handle various use cases. That's how macro-template was born.

Last but not least, I found seq-macro and borrowed some ideas from it, such as iterating over a range of numbers, characters, or bytes, and using a syntax #( ... )* to generate partial repeated substitutions. This eliminates the need for an extra template_match! to handle repetitions in match arms, and allows for more flexible code generation.

Go back to the beginning, why do you need macro_template::template! at all? Isn't it the same as a simple macro_rules!?

macro_rules! impl_serialize {
    ($($ty:ty),* $(,)?) => {
        $(
            impl serde_core::Serialize for BSize<$ty> {
                fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
                where
                    S: serde_core::Serializer,
                {
                    if ser.is_human_readable() {
                        ser.collect_str(self)
                    } else {
                        self.0.serialize(ser)
                    }
                }
            }
        )*
    };
}

impl_serialize!(u8, u16, u32, u64, usize);

Except that macro_template::template! supports more flexible substitution patterns as shown in the Examples section, template! has a concise syntax, and it saves you from declaring an extra macro_rules!, naming it, and invoking it.

The example above can be rewritten as:

macro_template::template! {
    for Ty in [u8, u16, u32, u64, usize] {
        impl serde_core::Serialize for BSize<Ty> {
            fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
            where
                S: serde_core::Serializer,
            {
                if ser.is_human_readable() {
                    ser.collect_str(self)
                } else {
                    self.0.serialize(ser)
                }
            }
        }
    }
}

Examples

First, you can generate code with a template and a matrix of values:

macro_template::template! {
    for (Endian, Method) in [
        (LittleEndian, to_le_bytes),
        (BigEndian, to_be_bytes),
        (NativeEndian, to_ne_bytes)
    ],
    for (Ty, Width) in [
        (u16, 2),
        (u32, 4),
    ],
    {
        impl StoreBytes<Endian, Width> for Ty {
            fn store_bytes(&self) -> [u8; Width] {
                self.Method()
            }
        }
    }
}

Without splice syntax, the whole template body is repeated once for each row. You can also do substitutions only partially with #( ... )*. Like quote, a single separator token tree can be written before *, such as #( ... ),*. For example, to generate match arms:

macro_template::template! {
    for T in [Int, Real, Double] {
        match Foo {
            #( EvalType::T => { panic!("{}", EvalType::T); } ),*,
            EvalType::Other => unreachable!(),
        }
    }
}

When a template contains #( ... )* or #( ... ),*, template variables are substituted only inside the splice body, and surrounding tokens are emitted once. Surrounding identifiers stay literal, even when they have the same name as a template variable. If a value should vary, place it in the splice body.

Naturally, if the match arm differs left-hand side and right-hand side:

macro_template::template! {
    for (K, Value) in [
        (Apple, "apple"),
        (Banana, "banana"),
        (Cherry, "cherry"),
    ] {
        match kind {
            #( Kind::K => { println!("{}", Value); } ),*
        }
    }
}

Inputs can also be ranges of numbers, characters, or bytes. Range inputs are written directly after in, without surrounding brackets:

// sequential numeric counter
let tuple = (1000, 100, 10);
let mut sum = 0;
macro_template::template! {
    for i in 0..3 {
        sum += tuple.i;
    }
}
assert_eq!(sum, 1110);

// sequential character collector
let mut string = String::new();
macro_template::template! {
    for c in 'x'..='z' {
        string.push(c);
    }
}
assert_eq!(string, "xyz");

Integer ranges preserve the radix, suffix, and shared padding width from their bounds.

You can combine multiple iterators in a single template:

let mut values = vec![];

macro_template::template! {
    for Prefix in ["read", "write"],
    for Code in 200..=201 {
        values.push((Prefix, Code));
    }
}

assert_eq!(
    values,
    [("read", 200), ("read", 201), ("write", 200), ("write", 201)],
);

Multiple input clauses form a Cartesian product in clause order.

Minimum Rust version policy

This crate's minimum supported rustc version is 1.85.0.

The current policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if crate 1.0 requires Rust 1.85.0, then crate 1.0.z for all values of z will also require Rust 1.85.0 or newer. However, crate 1.y for y > 0 may require a newer minimum version of Rust.

License

This project is licensed under Apache License, Version 2.0.