tatara-rust-macro-rules 0.1.3

L2 declarative-macro authoring — typed `MacroRulesSpec` compiles to a normal lib crate exposing `macro_rules! my_macro { … }`.
Documentation
//! `tatara-rust-macro-rules` — L2 authoring surface for **declarative** macros.
//!
//! One typed [`MacroRulesSpec`] value → one normal library crate that
//! exposes `pub macro_rules! my_macro { … }`. No proc-macro plumbing
//! required; this is a `[lib]` not a `[lib] proc-macro = true`.
//!
//! Each arm carries raw matcher + transcriber token text. Authoring
//! shape:
//!
//! ```
//! use tatara_rust_ast::{CompileToCrate, Ident};
//! use tatara_rust_macro_rules::{MacroArm, MacroRulesSpec};
//!
//! let spec = MacroRulesSpec {
//!     macro_name: Ident::new("my_vec"),
//!     arms: vec![MacroArm {
//!         matcher: "( $($e:expr),* $(,)? )".into(),
//!         transcriber: "{ ::std::vec![ $($e),* ] }".into(),
//!     }],
//! };
//! let scaffold = spec.compile_to_crate("my-vec-macros").unwrap();
//! ```

use serde::{Deserialize, Serialize};
use tatara_rust_ast::{AstError, CompileToCrate, CrateScaffold, Ident};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MacroRulesSpec {
    pub macro_name: Ident,
    pub arms: Vec<MacroArm>,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MacroArm {
    /// LHS pattern, including the outer delimiter. Raw token text;
    /// re-parsed by rustc at consumer compile time.
    pub matcher: String,
    /// RHS expansion, including the outer delimiter.
    pub transcriber: String,
}

impl CompileToCrate for MacroRulesSpec {
    fn compile_to_crate(&self, crate_name: &str) -> Result<CrateScaffold, AstError> {
        let mut scaffold = CrateScaffold::new(crate_name, "0.1.0");
        scaffold.add_file("Cargo.toml", render_cargo_toml(crate_name));
        scaffold.add_file("src/lib.rs", render_lib_rs(self));
        Ok(scaffold)
    }
}

fn render_cargo_toml(crate_name: &str) -> String {
    format!(
        r#"[package]
name = "{crate_name}"
version = "0.1.0"
edition = "2024"
license = "MIT"
description = "Declarative-macro crate emitted from a tatara-rust-macro-rules MacroRulesSpec."

[lib]
"#
    )
}

fn render_lib_rs(spec: &MacroRulesSpec) -> String {
    let mac = &spec.macro_name.0;
    let arms = spec
        .arms
        .iter()
        .map(|a| format!("    {} => {};", a.matcher, a.transcriber))
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        r#"// GENERATED by tatara-rust-macro-rules from a MacroRulesSpec.
#[macro_export]
macro_rules! {mac} {{
{arms}
}}
"#
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    fn sample() -> MacroRulesSpec {
        MacroRulesSpec {
            macro_name: Ident::new("my_vec"),
            arms: vec![MacroArm {
                matcher: "( $($e:expr),* $(,)? )".into(),
                transcriber: "{ ::std::vec![ $($e),* ] }".into(),
            }],
        }
    }

    #[test]
    fn compiles_to_lib_and_cargo() {
        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
        let files = scaffold.to_files();
        assert!(files.contains_key("Cargo.toml"));
        assert!(files.contains_key("src/lib.rs"));
    }

    #[test]
    fn lib_rs_emits_macro_export() {
        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert!(lib.contains("#[macro_export]"));
        assert!(lib.contains("macro_rules! my_vec"));
        assert!(lib.contains("( $($e:expr),* $(,)? )"));
        assert!(lib.contains("::std::vec!"));
    }

    #[test]
    fn cargo_toml_is_normal_lib_not_proc_macro() {
        let scaffold = sample().compile_to_crate("my-vec-macros").unwrap();
        let toml = scaffold.to_files().get("Cargo.toml").unwrap().clone();
        // No proc-macro line — declarative macros ship in normal libs.
        assert!(!toml.contains("proc-macro"));
    }

    #[test]
    fn multiple_arms_emit_in_order() {
        let spec = MacroRulesSpec {
            macro_name: Ident::new("two"),
            arms: vec![
                MacroArm {
                    matcher: "()".into(),
                    transcriber: "{ () }".into(),
                },
                MacroArm {
                    matcher: "($e:expr)".into(),
                    transcriber: "{ $e }".into(),
                },
            ],
        };
        let scaffold = spec.compile_to_crate("two-macros").unwrap();
        let lib = scaffold.to_files().get("src/lib.rs").unwrap().clone();
        assert!(lib.find("()").unwrap() < lib.find("($e:expr)").unwrap());
    }

    #[test]
    fn serde_roundtrip() {
        let s = sample();
        let j = serde_json::to_string(&s).unwrap();
        let back: MacroRulesSpec = serde_json::from_str(&j).unwrap();
        assert_eq!(s, back);
    }
}