1use std::path::Path;
34
35use crate::resolver::ResolvedConfig;
36use crate::{Error, Result};
37
38const HEADER_TEMPLATE: &str = include_str!("../templates/header.rs");
40
41const MACRO_TEMPLATE: &str = include_str!("../templates/macro.rs");
43
44pub fn generate(config: ResolvedConfig) -> Result<()> {
67 let out_dir =
68 std::env::var("OUT_DIR").map_err(|_| Error::Validation("OUT_DIR not set".to_string()))?;
69 let output_path = Path::new(&out_dir).join("derive_defs.rs");
70
71 generate_to(config, &output_path)
72}
73
74pub fn generate_to<P: AsRef<Path>>(config: ResolvedConfig, output: P) -> Result<()> {
95 let code = generate_code(config);
96
97 std::fs::write(output.as_ref(), code).map_err(Error::CodegenWrite)?;
98
99 Ok(())
100}
101
102fn generate_code(config: ResolvedConfig) -> String {
104 let mut code = String::new();
105
106 code.push_str(HEADER_TEMPLATE);
108 code.push('\n');
109
110 for (name, def) in config.defs {
112 let macro_name = name.replace('.', "_");
113
114 let derive_list = if def.traits.is_empty() {
116 String::new()
117 } else {
118 def.traits.join(", ")
119 };
120
121 let attr_list = def.attrs.join("\n");
123
124 code.push_str(&generate_macro(¯o_name, &derive_list, &attr_list));
126 code.push('\n');
127 }
128
129 code
130}
131
132#[allow(clippy::literal_string_with_formatting_args)]
134fn generate_macro(name: &str, derive_list: &str, attr_list: &str) -> String {
135 MACRO_TEMPLATE
136 .replace("{name}", name)
137 .replace("{derive_list:?}", &format!("{derive_list:?}"))
138 .replace("{attr_list:?}", &format!("{attr_list:?}"))
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::resolver::ResolvedDef;
145 use std::collections::HashMap;
146
147 #[test]
148 fn test_generate_code() {
149 let mut defs = HashMap::new();
150 defs.insert(
151 "serialization".to_string(),
152 ResolvedDef {
153 name: "serialization".to_string(),
154 traits: vec![
155 "Clone".to_string(),
156 "Serialize".to_string(),
157 "Deserialize".to_string(),
158 ],
159 attrs: vec!["#[serde(rename_all = \"camelCase\")]".to_string()],
160 },
161 );
162
163 let config = ResolvedConfig { defs };
164 let code = generate_code(config);
165
166 assert!(code.contains("proc_macro_attribute"));
167 assert!(code.contains("fn serialization"));
168 assert!(code.contains("#[derive(#(#derives),*)]"));
170 assert!(code.contains("serde(rename_all"));
171 assert!(code.contains("struct DefArgs"));
172 assert!(code.contains("impl Parse for DefArgs"));
173 assert!(code.contains("\"Clone, Serialize, Deserialize\""));
175 }
176
177 #[test]
183 fn test_generated_code_converts_traits_to_idents() {
184 let mut defs = HashMap::new();
185 defs.insert(
186 "model".to_string(),
187 ResolvedDef {
188 name: "model".to_string(),
189 traits: vec!["Debug".to_string(), "Clone".to_string()],
190 attrs: vec![],
191 },
192 );
193
194 let config = ResolvedConfig { defs };
195 let code = generate_code(config);
196
197 assert!(
200 code.contains("proc_macro2::Ident::new"),
201 "Generated code should convert trait names to proc_macro2::Ident"
202 );
203
204 assert!(
206 code.contains("proc_macro2::Span::call_site()"),
207 "Generated code should use call_site() span for Idents"
208 );
209
210 assert!(
212 code.contains("Vec<proc_macro2::Ident>"),
213 "Generated code should declare derives as Vec<proc_macro2::Ident>"
214 );
215 }
216
217 #[test]
222 fn test_generated_code_derive_construction() {
223 let mut defs = HashMap::new();
224 defs.insert(
225 "test".to_string(),
226 ResolvedDef {
227 name: "test".to_string(),
228 traits: vec!["Debug".to_string()],
229 attrs: vec![],
230 },
231 );
232
233 let config = ResolvedConfig { defs };
234 let code = generate_code(config);
235
236 assert!(
238 code.contains("let derives: Vec<proc_macro2::Ident>"),
239 "Generated code should create a typed Vec of Idents for derives"
240 );
241
242 assert!(
248 code.contains("final_derives") && code.contains(".iter()"),
249 "Generated code should iterate over final_derives"
250 );
251
252 assert!(
254 code.contains("#(#derives),*"),
255 "Generated code should use derives in quote! macro"
256 );
257 }
258
259 #[test]
260 fn test_generate_code_empty_traits() {
261 let mut defs = HashMap::new();
262 defs.insert(
263 "empty".to_string(),
264 ResolvedDef {
265 name: "empty".to_string(),
266 traits: vec![],
267 attrs: vec!["#[repr(C)]".to_string()],
268 },
269 );
270
271 let config = ResolvedConfig { defs };
272 let code = generate_code(config);
273
274 assert!(code.contains("fn empty"));
275 assert!(!code.contains("derive()")); assert!(code.contains("repr(C)"));
277
278 assert!(
280 code.contains("let derive_attr = if final_derives.is_empty()"),
281 "Generated code should check for empty derives"
282 );
283 }
284
285 #[test]
286 fn test_generate_code_namespaced() {
287 let mut defs = HashMap::new();
288 defs.insert(
289 "common.serialization".to_string(),
290 ResolvedDef {
291 name: "common.serialization".to_string(),
292 traits: vec!["Clone".to_string()],
293 attrs: vec![],
294 },
295 );
296
297 let config = ResolvedConfig { defs };
298 let code = generate_code(config);
299
300 assert!(code.contains("fn common_serialization"));
302 }
303
304 #[test]
306 fn test_generated_code_includes_helpers() {
307 let config = ResolvedConfig {
308 defs: HashMap::new(),
309 };
310 let code = generate_code(config);
311
312 assert!(
313 code.contains("fn parse_trait_list"),
314 "Generated code should include parse_trait_list function"
315 );
316 assert!(
317 code.contains("fn filter_attrs"),
318 "Generated code should include filter_attrs function"
319 );
320 }
321
322 #[test]
324 fn test_generated_code_handles_runtime_mods() {
325 let mut defs = HashMap::new();
326 defs.insert(
327 "configurable".to_string(),
328 ResolvedDef {
329 name: "configurable".to_string(),
330 traits: vec!["Debug".to_string(), "Clone".to_string()],
331 attrs: vec![],
332 },
333 );
334
335 let config = ResolvedConfig { defs };
336 let code = generate_code(config);
337
338 assert!(
340 code.contains("omit_traits"),
341 "Generated code should handle omit_traits"
342 );
343
344 assert!(
346 code.contains("add_traits"),
347 "Generated code should handle add_traits"
348 );
349
350 assert!(
352 code.contains(".filter(|t| !omit_traits.contains(t))"),
353 "Generated code should filter omitted traits"
354 );
355 }
356}