1use serde::{Deserialize, Serialize};
37use tatara_rust_ast::{AstError, CompileToCrate, CrateScaffold, Ident};
38
39#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum AttrValueKind {
42 Str,
44 Int,
46 Bool,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51pub struct AttrKnob {
52 pub name: String,
54 pub kind: AttrValueKind,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub default: Option<String>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63pub struct PerAttrDeriveSpec {
64 pub trait_name: Ident,
65 pub knobs: Vec<AttrKnob>,
66 pub per_field_template: String,
69}
70
71impl PerAttrDeriveSpec {
72 fn fn_name(&self) -> String {
73 let s = &self.trait_name.0;
74 let mut out = String::from("derive_");
75 for (i, c) in s.chars().enumerate() {
76 if c.is_uppercase() {
77 if i > 0 {
78 out.push('_');
79 }
80 out.extend(c.to_lowercase());
81 } else {
82 out.push(c);
83 }
84 }
85 out
86 }
87}
88
89impl CompileToCrate for PerAttrDeriveSpec {
90 fn compile_to_crate(&self, crate_name: &str) -> Result<CrateScaffold, AstError> {
91 let mut s = CrateScaffold::new(crate_name, "0.1.0");
92 s.add_file("Cargo.toml", render_cargo_toml(crate_name));
93 s.add_file("src/lib.rs", render_lib_rs(self));
94 Ok(s)
95 }
96}
97
98fn render_cargo_toml(crate_name: &str) -> String {
99 tatara_rust_ast::render_proc_macro_cargo_toml(
100 crate_name,
101 "Per-attr derive proc-macro emitted from a tatara-rust-attr PerAttrDeriveSpec.",
102 )
103}
104
105fn render_lib_rs(spec: &PerAttrDeriveSpec) -> String {
106 let trait_id = &spec.trait_name.0;
107 let fn_name = spec.fn_name();
108 let trait_lower = trait_id.to_lowercase();
109 let per_field_tpl = &spec.per_field_template;
110
111 let mut knob_let = String::new();
115 let mut knob_parse = String::new();
116 for k in &spec.knobs {
117 let n = &k.name;
118 let default_expr = match (&k.default, &k.kind) {
119 (Some(d), AttrValueKind::Str) => format!("{d:?}.to_string()"),
120 (Some(d), AttrValueKind::Int) => d.clone(),
121 (Some(d), AttrValueKind::Bool) => d.clone(),
122 (None, AttrValueKind::Str) => "String::new()".to_string(),
123 (None, AttrValueKind::Int) => "0i64".to_string(),
124 (None, AttrValueKind::Bool) => "false".to_string(),
125 };
126 knob_let.push_str(&format!(" let mut {n} = {default_expr};\n"));
127 let parse_arm = match k.kind {
128 AttrValueKind::Str => format!(
129 " if path.is_ident({n:?}) {{ if let syn::Expr::Lit(syn::ExprLit {{ lit: syn::Lit::Str(s), .. }}) = &mv.value {{ {n} = s.value(); }} }}"
130 ),
131 AttrValueKind::Int => format!(
132 " if path.is_ident({n:?}) {{ if let syn::Expr::Lit(syn::ExprLit {{ lit: syn::Lit::Int(i), .. }}) = &mv.value {{ {n} = i.base10_parse::<i64>().unwrap_or(0); }} }}"
133 ),
134 AttrValueKind::Bool => format!(
135 " if path.is_ident({n:?}) {{ if let syn::Expr::Lit(syn::ExprLit {{ lit: syn::Lit::Bool(b), .. }}) = &mv.value {{ {n} = b.value; }} }}"
136 ),
137 };
138 knob_parse.push_str(&parse_arm);
139 knob_parse.push('\n');
140 }
141
142 let mut out = String::new();
143 out.push_str("// GENERATED by tatara-rust-attr::PerAttrDeriveSpec.\n");
144 out.push_str("use proc_macro::TokenStream;\n");
145 out.push_str("use quote::quote;\n");
146 out.push_str(
147 "use syn::{Data, DataStruct, DeriveInput, Fields, Meta, parse_macro_input};\n\n",
148 );
149 out.push_str(&format!(
150 "#[proc_macro_derive({trait_id}, attributes({trait_lower}))]\n"
151 ));
152 out.push_str(&format!(
153 "pub fn {fn_name}(input: TokenStream) -> TokenStream {{\n"
154 ));
155 out.push_str(" let input = parse_macro_input!(input as DeriveInput);\n");
156 out.push_str(" let self_name = &input.ident;\n\n");
157 out.push_str(&knob_let);
158 out.push('\n');
159 out.push_str(&format!(
160 " for attr in &input.attrs {{\n if attr.path().is_ident({trait_lower:?}) {{\n"
161 ));
162 out.push_str(" if let Ok(metas) = attr.parse_args_with(syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated) {\n");
163 out.push_str(" for meta in metas {\n");
164 out.push_str(" let Meta::NameValue(mv) = &meta else { continue };\n");
165 out.push_str(" let path = mv.path.clone();\n");
166 out.push_str(&knob_parse);
167 out.push_str(" }\n");
168 out.push_str(" }\n");
169 out.push_str(" }\n");
170 out.push_str(" }\n\n");
171 out.push_str(" let fields = match &input.data {\n");
172 out.push_str(
173 " Data::Struct(DataStruct { fields: Fields::Named(named), .. }) => &named.named,\n",
174 );
175 out.push_str(" _ => return syn::Error::new_spanned(self_name, \"PerAttrDerive requires a named-fields struct\").to_compile_error().into(),\n");
176 out.push_str(" };\n\n");
177 out.push_str(" let per_field = fields.iter().map(|f| {\n");
178 out.push_str(" let field_name = f.ident.as_ref().expect(\"named field\");\n");
179 out.push_str(" let field_ty = &f.ty;\n");
180 for k in &spec.knobs {
183 let n = &k.name;
184 match k.kind {
185 AttrValueKind::Str => {
186 out.push_str(&format!(
187 " let {n}_ident = quote::format_ident!(\"{{}}{{}}\", {n}, field_name.to_string());\n"
188 ));
189 out.push_str(&format!(
190 " let {n} = &{n}_ident;\n"
191 ));
192 }
193 AttrValueKind::Int | AttrValueKind::Bool => {
194 out.push_str(&format!(" let {n} = &{n};\n"));
195 }
196 }
197 }
198 out.push_str(" quote! {\n");
199 out.push_str(" ");
200 out.push_str(per_field_tpl);
201 out.push_str("\n }\n");
202 out.push_str(" });\n\n");
203 out.push_str(" let expanded = quote! {\n");
204 out.push_str(" impl #self_name {\n");
205 out.push_str(" #(#per_field)*\n");
206 out.push_str(" }\n");
207 out.push_str(" };\n");
208 out.push_str(" TokenStream::from(expanded)\n");
209 out.push_str("}\n");
210 out
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn prefixed() -> PerAttrDeriveSpec {
218 PerAttrDeriveSpec {
219 trait_name: Ident::new("Prefixed"),
220 knobs: vec![AttrKnob {
221 name: "prefix".into(),
222 kind: AttrValueKind::Str,
223 default: Some("with_".into()),
224 }],
225 per_field_template:
226 "pub fn #prefix(self, v: #field_ty) -> Self { self }".into(),
227 }
228 }
229
230 #[test]
231 fn compiles_to_lib_and_cargo() {
232 let s = prefixed().compile_to_crate("prefixed-derive").unwrap();
233 let files = s.to_files();
234 assert!(files.contains_key("Cargo.toml"));
235 assert!(files.contains_key("src/lib.rs"));
236 }
237
238 #[test]
239 fn proc_macro_derive_declares_attribute() {
240 let s = prefixed().compile_to_crate("p").unwrap();
241 let lib = s.to_files().get("src/lib.rs").unwrap().clone();
242 assert!(lib.contains("#[proc_macro_derive(Prefixed, attributes(prefixed))]"));
244 }
245
246 #[test]
247 fn lib_rs_initializes_default_and_parses_knob() {
248 let s = prefixed().compile_to_crate("p").unwrap();
249 let lib = s.to_files().get("src/lib.rs").unwrap().clone();
250 assert!(lib.contains("let mut prefix"));
251 assert!(lib.contains(r#""with_""#));
252 assert!(lib.contains(r#"path.is_ident("prefix")"#));
253 assert!(lib.contains("syn::Lit::Str"));
254 }
255
256 #[test]
257 fn string_knob_format_idents_for_prefix_concat() {
258 let s = prefixed().compile_to_crate("p").unwrap();
259 let lib = s.to_files().get("src/lib.rs").unwrap().clone();
260 assert!(lib.contains("let prefix_ident = quote::format_ident!"));
261 }
262
263 #[test]
264 fn multiple_knobs_each_get_default_let() {
265 let mut s = prefixed();
266 s.knobs.push(AttrKnob {
267 name: "inline".into(),
268 kind: AttrValueKind::Bool,
269 default: Some("false".into()),
270 });
271 s.knobs.push(AttrKnob {
272 name: "max".into(),
273 kind: AttrValueKind::Int,
274 default: Some("10".into()),
275 });
276 let lib = s.compile_to_crate("p").unwrap().to_files().get("src/lib.rs").unwrap().clone();
277 assert!(lib.contains("let mut prefix"));
278 assert!(lib.contains("let mut inline"));
279 assert!(lib.contains("let mut max"));
280 assert!(lib.contains("syn::Lit::Bool"));
281 assert!(lib.contains("syn::Lit::Int"));
282 }
283
284 #[test]
285 fn serde_roundtrip() {
286 let s = prefixed();
287 let j = serde_json::to_string(&s).unwrap();
288 let back: PerAttrDeriveSpec = serde_json::from_str(&j).unwrap();
289 assert_eq!(s, back);
290 }
291}