diff options
| -rw-r--r-- | Cargo.lock | 61 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | just_template/Cargo.toml | 1 | ||||
| -rw-r--r-- | just_template/src/deprecated.rs | 45 | ||||
| -rw-r--r-- | just_template/src/lib.rs | 60 | ||||
| -rw-r--r-- | just_template/src/template.rs | 9 | ||||
| -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.rs | 186 | ||||
| -rw-r--r-- | just_template_macros/Cargo.toml | 14 | ||||
| -rw-r--r-- | just_template_macros/src/lib.rs | 206 |
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" @@ -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),* + ])); + } + } + } +} |
