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 format!("#[derive({})]", 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(Clone, Serialize, Deserialize)"));
169 assert!(code.contains("serde(rename_all"));
170 assert!(code.contains("struct DefArgs"));
171 assert!(code.contains("impl Parse for DefArgs"));
172 }
173
174 #[test]
180 fn test_generated_code_converts_traits_to_idents() {
181 let mut defs = HashMap::new();
182 defs.insert(
183 "model".to_string(),
184 ResolvedDef {
185 name: "model".to_string(),
186 traits: vec!["Debug".to_string(), "Clone".to_string()],
187 attrs: vec![],
188 },
189 );
190
191 let config = ResolvedConfig { defs };
192 let code = generate_code(config);
193
194 assert!(
197 code.contains("proc_macro2::Ident::new"),
198 "Generated code should convert trait names to proc_macro2::Ident"
199 );
200
201 assert!(
203 code.contains("proc_macro2::Span::call_site()"),
204 "Generated code should use call_site() span for Idents"
205 );
206
207 assert!(
209 code.contains("Vec<proc_macro2::Ident>"),
210 "Generated code should declare derives as Vec<proc_macro2::Ident>"
211 );
212 }
213
214 #[test]
219 fn test_generated_code_derive_construction() {
220 let mut defs = HashMap::new();
221 defs.insert(
222 "test".to_string(),
223 ResolvedDef {
224 name: "test".to_string(),
225 traits: vec!["Debug".to_string()],
226 attrs: vec![],
227 },
228 );
229
230 let config = ResolvedConfig { defs };
231 let code = generate_code(config);
232
233 assert!(
235 code.contains("let derives: Vec<proc_macro2::Ident>"),
236 "Generated code should create a typed Vec of Idents for derives"
237 );
238
239 assert!(
245 code.contains("final_derives") && code.contains(".iter()"),
246 "Generated code should iterate over final_derives"
247 );
248
249 assert!(
251 code.contains("#(#derives),*"),
252 "Generated code should use derives in quote! macro"
253 );
254 }
255
256 #[test]
257 fn test_generate_code_empty_traits() {
258 let mut defs = HashMap::new();
259 defs.insert(
260 "empty".to_string(),
261 ResolvedDef {
262 name: "empty".to_string(),
263 traits: vec![],
264 attrs: vec!["#[repr(C)]".to_string()],
265 },
266 );
267
268 let config = ResolvedConfig { defs };
269 let code = generate_code(config);
270
271 assert!(code.contains("fn empty"));
272 assert!(!code.contains("derive()")); assert!(code.contains("repr(C)"));
274
275 assert!(
277 code.contains("let derive_attr = if final_derives.is_empty()"),
278 "Generated code should check for empty derives"
279 );
280 }
281
282 #[test]
283 fn test_generate_code_namespaced() {
284 let mut defs = HashMap::new();
285 defs.insert(
286 "common.serialization".to_string(),
287 ResolvedDef {
288 name: "common.serialization".to_string(),
289 traits: vec!["Clone".to_string()],
290 attrs: vec![],
291 },
292 );
293
294 let config = ResolvedConfig { defs };
295 let code = generate_code(config);
296
297 assert!(code.contains("fn common_serialization"));
299 }
300
301 #[test]
303 fn test_generated_code_includes_helpers() {
304 let config = ResolvedConfig {
305 defs: HashMap::new(),
306 };
307 let code = generate_code(config);
308
309 assert!(
310 code.contains("fn parse_trait_list"),
311 "Generated code should include parse_trait_list function"
312 );
313 assert!(
314 code.contains("fn filter_attrs"),
315 "Generated code should include filter_attrs function"
316 );
317 }
318
319 #[test]
321 fn test_generated_code_handles_runtime_mods() {
322 let mut defs = HashMap::new();
323 defs.insert(
324 "configurable".to_string(),
325 ResolvedDef {
326 name: "configurable".to_string(),
327 traits: vec!["Debug".to_string(), "Clone".to_string()],
328 attrs: vec![],
329 },
330 );
331
332 let config = ResolvedConfig { defs };
333 let code = generate_code(config);
334
335 assert!(
337 code.contains("omit_traits"),
338 "Generated code should handle omit_traits"
339 );
340
341 assert!(
343 code.contains("add_traits"),
344 "Generated code should handle add_traits"
345 );
346
347 assert!(
349 code.contains(".filter(|t| !omit_traits.contains(t))"),
350 "Generated code should filter omitted traits"
351 );
352 }
353}