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 {
pub matcher: String,
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();
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);
}
}