aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-29 03:54:54 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-29 03:54:54 +0800
commitca7527681b609fedc368ea973022b004469035e6 (patch)
tree47109782fa5a4435889d93a823db3a4e1f5e7bd6
parentba15b7c06468cb6c52c8d2a53419fd83f9ebcb8b (diff)
feat(just_template): add proc-macro `tmpl!` and restructure crate
Move the old `tmpl!` and `tmpl_param!` macros into a dedicated `just_template_macros` proc-macro crate. The new `tmpl!` macro supports both simple parameter assignment and multi-arm implementation blocks with per-arm overrides. Remove the deprecated `deprecated` module and update tests accordingly.
-rw-r--r--Cargo.lock61
-rw-r--r--Cargo.toml5
-rw-r--r--just_template/Cargo.toml1
-rw-r--r--just_template/src/deprecated.rs45
-rw-r--r--just_template/src/lib.rs60
-rw-r--r--just_template/src/template.rs9
-rw-r--r--just_template/src/test_expand.rs (renamed from just_template/src/test.rs)2
-rw-r--r--just_template/src/test_macros.rs186
-rw-r--r--just_template_macros/Cargo.toml14
-rw-r--r--just_template_macros/src/lib.rs206
10 files changed, 491 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2560cb5
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,61 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_template"
+version = "0.2.0"
+dependencies = [
+ "just_fmt",
+ "just_template_macros",
+]
+
+[[package]]
+name = "just_template_macros"
+version = "0.2.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/Cargo.toml b/Cargo.toml
index 157065c..b0aed88 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,5 @@
[workspace]
+resolver = "3"
members = [
"just_template",
"just_template_macros",
@@ -9,3 +10,7 @@ version = "0.2.0"
description = "A tool for code gen via templatese"
authors = ["Weicao-CatilGrass <catil_grass@qq.com>"]
edition = "2024"
+
+[workspace.dependencies]
+just_template = { path = "just_template" }
+just_template_macros = { path = "just_template_macros" }
diff --git a/just_template/Cargo.toml b/just_template/Cargo.toml
index 66217b4..1536e0b 100644
--- a/just_template/Cargo.toml
+++ b/just_template/Cargo.toml
@@ -10,4 +10,5 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/catilgrass/just_template"
[dependencies]
+just_template_macros.workspace = true
just_fmt = "0.1.2"
diff --git a/just_template/src/deprecated.rs b/just_template/src/deprecated.rs
deleted file mode 100644
index b218969..0000000
--- a/just_template/src/deprecated.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-#[macro_export]
-macro_rules! tmpl_param {
- ($template:ident, $($key:ident = $value:expr),* $(,)?) => {{
- $(
- $template.insert_param(stringify!($key).to_string(), $value.to_string());
- )*
- }};
-}
-
-#[macro_export]
-macro_rules! tmpl {
- ($template:ident, $($name:ident {
- $($key:ident = $value:expr),* $(,)?
- }),* $(,)?) => {{
- $(
- let $name = $template.add_impl(stringify!($name).to_string());
- $(
- $name.push({
- let mut params = std::collections::HashMap::new();
- params.insert(stringify!($key).to_string(), $value.to_string());
- params
- });
- )*
- )*
- }};
-
- // Old syntax
- ($template:ident += {
- $($name:ident {
- $(($($key:ident = $value:expr),* $(,)?)),*
- $(,)?
- }),*
- }) => {{
- $(
- let $name = $template.add_impl(stringify!($name).to_string());
- $(
- $name.push({
- let mut params = std::collections::HashMap::new();
- $(params.insert(stringify!($key).to_string(), $value.to_string());)*
- params
- });
- )*
- )*
- }};
-}
diff --git a/just_template/src/lib.rs b/just_template/src/lib.rs
index 7ff77f5..2b4f986 100644
--- a/just_template/src/lib.rs
+++ b/just_template/src/lib.rs
@@ -1,58 +1,14 @@
//! Template struct for storing template strings and their parameters.
-//!
-//! The template supports two types of parameters:
-//! - Simple parameters: key-value pairs used to replace simple placeholders (`<<<key>>>` format) in the template.
-//! - Implementation parameters: for implementation blocks (`>>>>>>>>> block_name` and `@@@ >>> block_name` format),
-//! can contain multiple parameter sets, each corresponding to an implementation instance.
-//!
-//! # Examples
-//! ```
-//! use just_template::Template;
-//!
-//! let mut tmpl = Template::from("Hello, <<<name>>>!".to_string());
-//! tmpl.insert_param("name".to_string(), "World".to_string());
-//! assert_eq!(tmpl.to_string(), "Hello, World!");
-//! ```
-//!
-//! Using the `tmpl_param!` macro makes it easier to add simple parameters:
-//! ```
-//! use just_template::{Template, tmpl_param};
-//!
-//! let mut tmpl = Template::from("<<<a>>> + <<<b>>> = <<<c>>>".to_string());
-//! tmpl_param!(tmpl, a = 1, b = 2, c = 3);
-//! assert_eq!(tmpl.to_string(), "1 + 2 = 3");
-//! ```
-//!
-//! Using the `tmpl!` macro adds implementation block parameters:
-//! ```
-//! use just_template::{Template, tmpl};
-//!
-//! let mut tmpl = Template::from("
-//! >>>>>>>>>> arms
-//! @@@ >>> arms
-//! <<<crate_name>>> => Some(<<<crate_name>>>::exec(data, params).await),
-//! @@@ <<<
-//! ".trim().to_string());
-//! tmpl!(tmpl,
-//! arms {
-//! crate_name = "my",
-//! crate_name = "you",
-//! }
-//! );
-//! // Output the expanded template
-//! let expanded = tmpl.to_string();
-//! assert_eq!(expanded, "
-//! my => Some(my::exec(data, params).await),
-//! you => Some(you::exec(data, params).await),
-//! ".trim().to_string());
-//! ```
+
+mod expand;
+
mod template;
-pub use template::*; // Re-export template to just_template
+pub use template::*; // Re-export template
-pub mod expand;
+pub use just_template_macros::*; // Re-export macros
#[cfg(test)]
-pub mod test;
+pub mod test_expand;
-#[deprecated]
-pub mod deprecated;
+#[cfg(test)]
+pub mod test_macros;
diff --git a/just_template/src/template.rs b/just_template/src/template.rs
index e368917..953d7a5 100644
--- a/just_template/src/template.rs
+++ b/just_template/src/template.rs
@@ -1,9 +1,18 @@
use std::collections::HashMap;
+/// Template struct, used to represent a template composed of a template string and parameters.
+///
+/// # Fields
+/// - `template_str` - Template string containing placeholders.
+/// - `params` - Normal parameters, key-value mappings.
+/// - `impl_params` - Implementation block parameters, where each implementation block name corresponds to a list of parameter maps (each parameter map is a key-value mapping).
#[derive(Default, Clone)]
pub struct Template {
+ /// Template string containing placeholders such as `{{param_name}}`.
pub(crate) template_str: String,
+ /// Normal parameters, keyed by parameter name with values as parameter values.
pub(crate) params: HashMap<String, String>,
+ /// Implementation block parameters, keyed by implementation block name with values as a list of parameter maps.
pub(crate) impl_params: HashMap<String, Vec<HashMap<String, String>>>,
}
diff --git a/just_template/src/test.rs b/just_template/src/test_expand.rs
index ca7b357..9132f6e 100644
--- a/just_template/src/test.rs
+++ b/just_template/src/test_expand.rs
@@ -1,6 +1,6 @@
use std::collections::HashMap;
-use crate::template::Template;
+use crate::Template;
#[test]
fn basic_param() {
diff --git a/just_template/src/test_macros.rs b/just_template/src/test_macros.rs
new file mode 100644
index 0000000..5be58f5
--- /dev/null
+++ b/just_template/src/test_macros.rs
@@ -0,0 +1,186 @@
+use crate::Template;
+use crate::tmpl;
+
+#[test]
+fn basic_param() {
+ let mut tmpl = Template::from("Hello, <<<name>>>!".to_string());
+
+ tmpl!(tmpl, name = "World");
+ // tmpl.insert_param("name".to_string(), "World".to_string());
+
+ assert_eq!(tmpl.expand().unwrap(), "Hello, World!");
+}
+
+#[test]
+fn multi_param() {
+ let mut tmpl = Template::from("<<<a>>> + <<<b>>> = <<<c>>>".to_string());
+
+ tmpl! {
+ // tmpl, // Template named tmpl can be omitted
+ a = "1",
+ b = "2",
+ c = "3",
+ };
+
+ assert_eq!(tmpl.expand().unwrap(), "1 + 2 = 3");
+}
+
+#[test]
+fn impl_blocks() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ "<<<crate_name>>>" => Some(<<<crate_name>>>::exec(data, params).await),
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl! {
+ arms {
+ crate_name = "my",
+ crate_name = "you",
+ }
+ }
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#""my" => Some(my::exec(data, params).await)"#));
+ assert!(expanded.contains(r#""you" => Some(you::exec(data, params).await)"#));
+}
+
+#[test]
+fn display_block_global_hidden_by_default() {
+ let tmpl = Template::from(
+ r#"
+visible line
+??? >>> debug
+ hidden line
+??? <<<
+visible end
+"#
+ .trim()
+ .to_string(),
+ );
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains("visible line"));
+ assert!(expanded.contains("visible end"));
+ assert!(!expanded.contains("hidden line"));
+}
+
+#[test]
+fn display_block_global_shown_via_param() {
+ let mut tmpl = Template::from(
+ r#"
+visible line
+??? >>> debug
+ shown line
+??? <<<
+visible end
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl! {
+ debug = true,
+ }
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains("visible line"));
+ assert!(expanded.contains("visible end"));
+ assert!(expanded.contains("shown line"));
+}
+
+#[test]
+fn display_block_inside_impl_area_hidden_by_default() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl! {
+ arms {
+ crate_name = "my"
+ }
+ }
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(!expanded.contains(r#"my => metrics"#));
+}
+
+#[test]
+fn display_block_inside_impl_area_shown_by_global_param() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl! {
+ extra = true,
+ arms {
+ crate_name = "my",
+ crate_name = "you"
+ }
+ }
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(expanded.contains(r#"my => metrics"#));
+ assert!(expanded.contains(r#"you => exec"#));
+ assert!(expanded.contains(r#"you => metrics"#));
+}
+
+#[test]
+fn display_block_inside_impl_area_shown_by_arm_param() {
+ let mut tmpl = Template::from(
+ r#"
+>>>>>>>>>> arms
+@@@ >>> arms
+ <<<crate_name>>> => exec,
+??? >>> extra
+ <<<crate_name>>> => metrics,
+??? <<<
+@@@ <<<
+"#
+ .trim()
+ .to_string(),
+ );
+
+ tmpl! {
+ arms {
+ crate_name = "my",
+ {
+ crate_name = "you",
+ extra = true
+ }
+ }
+ }
+
+ let expanded = tmpl.expand().unwrap();
+ assert!(expanded.contains(r#"my => exec"#));
+ assert!(!expanded.contains(r#"my => metrics"#));
+ assert!(expanded.contains(r#"you => exec"#));
+ assert!(expanded.contains(r#"you => metrics"#));
+}
diff --git a/just_template_macros/Cargo.toml b/just_template_macros/Cargo.toml
new file mode 100644
index 0000000..c033d1b
--- /dev/null
+++ b/just_template_macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "just_template_macros"
+version.workspace = true
+description.workspace = true
+authors.workspace = true
+edition.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+proc-macro2 = "1.0.106"
+quote = "1.0.46"
+syn = { version = "2.0.118", features = ["full", "extra-traits"] }
diff --git a/just_template_macros/src/lib.rs b/just_template_macros/src/lib.rs
new file mode 100644
index 0000000..3f0b4fb
--- /dev/null
+++ b/just_template_macros/src/lib.rs
@@ -0,0 +1,206 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::parse::{Parse, ParseStream};
+use syn::{Expr, Ident, Token, braced};
+
+// ── Parsing ─────────────────────────────────────────────────────────────────
+
+/// Top-level item inside `tmpl! { }` or after the template ident in `tmpl!(tmpl, ...)`
+enum TopItem {
+ /// `key = value` — simple parameter
+ Param { key: Ident, value: Expr },
+ /// `name { arm, arm, ... }` — implementation block
+ ImplBlock { name: Ident, arms: Vec<Arm> },
+}
+
+/// An arm inside an implementation block
+enum Arm {
+ /// `key = value` — single-param arm
+ Single { key: Ident, value: Expr },
+ /// `{ key = value, key = value, ... }` — multi-param arm
+ Multi(Vec<(Ident, Expr)>),
+}
+
+// ── Parse implementations ──────────────────────────────────────────────────
+
+impl Parse for TopItem {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ let ident: Ident = input.parse()?;
+ let ahead = input.lookahead1();
+ if ahead.peek(Token![=]) {
+ let _eq: Token![=] = input.parse()?;
+ let value: Expr = input.parse()?;
+ Ok(TopItem::Param { key: ident, value })
+ } else if ahead.peek(syn::token::Brace) {
+ let content;
+ braced!(content in input);
+ let arms: Vec<Arm> = content
+ .parse_terminated(Arm::parse, Token![,])?
+ .into_iter()
+ .collect();
+ Ok(TopItem::ImplBlock { name: ident, arms })
+ } else {
+ Err(ahead.error())
+ }
+ }
+}
+
+impl Parse for Arm {
+ fn parse(input: ParseStream) -> syn::Result<Self> {
+ if input.peek(syn::token::Brace) {
+ let content;
+ braced!(content in input);
+ let params: Vec<(Ident, Expr)> = content
+ .parse_terminated(Self::parse_single, Token![,])?
+ .into_iter()
+ .collect();
+ Ok(Arm::Multi(params))
+ } else {
+ let (key, value) = Self::parse_single(input)?;
+ Ok(Arm::Single { key, value })
+ }
+ }
+}
+
+impl Arm {
+ fn parse_single(input: ParseStream) -> syn::Result<(Ident, Expr)> {
+ let key: Ident = input.parse()?;
+ let _eq: Token![=] = input.parse()?;
+ let value: Expr = input.parse()?;
+ Ok((key, value))
+ }
+}
+
+// ── Macro entry point ──────────────────────────────────────────────────────
+
+fn split_input(input: ParseStream) -> syn::Result<(Ident, Vec<TopItem>)> {
+ // Try to parse: `ident , items` (explicit template variable)
+ // If the first token is an ident followed by `,`, it's explicit.
+ // Otherwise, default to `tmpl` and parse as items.
+
+ let first: Ident = input.parse()?;
+ if input.peek(Token![,]) {
+ let _comma: Token![,] = input.parse()?;
+ let items: Vec<TopItem> = input
+ .parse_terminated(TopItem::parse, Token![,])?
+ .into_iter()
+ .collect();
+ Ok((first, items))
+ } else {
+ // The first ident is actually the start of the first item.
+ // We need to re-parse. Use `syn::Result` to signal this.
+ // Tricky: we've already consumed `first`. Since syn::parse uses
+ // ParseStream which is a cursor, we actually can't rewind easily.
+ //
+ // Solution: use a custom approach — treat the first ident as
+ // the start of an item, and parse the rest as items.
+ let items = {
+ // Re-build: first ident + rest
+ let mut items: Vec<TopItem> = Vec::new();
+
+ // Parse the first item starting with `first`
+ let ahead = input.lookahead1();
+ if ahead.peek(Token![=]) {
+ let _eq: Token![=] = input.parse()?;
+ let value: Expr = input.parse()?;
+ items.push(TopItem::Param { key: first, value });
+ } else if ahead.peek(syn::token::Brace) {
+ let content;
+ braced!(content in input);
+ let arms: Vec<Arm> = content
+ .parse_terminated(Arm::parse, Token![,])?
+ .into_iter()
+ .collect();
+ items.push(TopItem::ImplBlock { name: first, arms });
+ } else {
+ return Err(ahead.error());
+ }
+
+ // Parse remaining items
+ while !input.is_empty() {
+ let _comma: Token![,] = input.parse()?;
+ if input.is_empty() {
+ break; // trailing comma
+ }
+ items.push(input.parse()?);
+ }
+
+ items
+ };
+
+ Ok((Ident::new("tmpl", proc_macro2::Span::call_site()), items))
+ }
+}
+
+#[proc_macro]
+pub fn tmpl(input: TokenStream) -> TokenStream {
+ match syn::parse::Parser::parse(split_input, input) {
+ Ok((template_var, items)) => {
+ let stmts = generate(&template_var, &items);
+ let expanded = quote! { { #(#stmts)* } };
+ expanded.into()
+ }
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// ── Code generation ────────────────────────────────────────────────────────
+
+fn generate(template_var: &Ident, items: &[TopItem]) -> Vec<proc_macro2::TokenStream> {
+ let mut stmts: Vec<proc_macro2::TokenStream> = Vec::new();
+
+ for item in items {
+ match item {
+ TopItem::Param { key, value } => {
+ let key_str = key.to_string();
+ stmts.push(quote! {
+ #template_var.insert_param(
+ #key_str.to_string(),
+ ::std::string::ToString::to_string(&#value),
+ );
+ });
+ }
+ TopItem::ImplBlock { name, arms } => {
+ let name_str = name.to_string();
+ // Generate push statements for each arm
+ let push_stmts: Vec<proc_macro2::TokenStream> =
+ arms.iter().map(|arm| gen_arm_push(name, arm)).collect();
+ stmts.push(quote! {
+ let #name = #template_var.add_impl(#name_str.to_string());
+ #(#push_stmts)*
+ });
+ }
+ }
+ }
+
+ stmts
+}
+
+fn gen_arm_push(name: &Ident, arm: &Arm) -> proc_macro2::TokenStream {
+ match arm {
+ Arm::Single { key, value } => {
+ let key_str = key.to_string();
+ quote! {
+ #name.push(::std::collections::HashMap::from([
+ (#key_str.to_string(), ::std::string::ToString::to_string(&#value)),
+ ]));
+ }
+ }
+ Arm::Multi(params) => {
+ let entries: Vec<_> = params
+ .iter()
+ .map(|(key, value)| {
+ let k = key.to_string();
+ quote! {
+ (#k.to_string(), ::std::string::ToString::to_string(&#value))
+ }
+ })
+ .collect();
+ quote! {
+ #name.push(::std::collections::HashMap::from([
+ #(#entries),*
+ ]));
+ }
+ }
+ }
+}