1use anyhow::Result;
2use std::collections::BTreeMap;
3use std::env;
4use std::fs;
5use std::path::PathBuf;
6use tera::Context;
7
8use crate::pipeline::Pipeline;
9use crate::template::Template;
10
11pub struct GenContext {
12 pub template_path: PathBuf,
13 pub output_root: PathBuf,
14 pub vars: BTreeMap<String, String>,
15 pub global_prefixes: BTreeMap<String, String>,
16 pub base: Option<String>,
17 pub dry_run: bool,
18}
19
20impl GenContext {
21 pub fn new(template_path: PathBuf, output_root: PathBuf) -> Self {
22 Self {
23 template_path,
24 output_root,
25 vars: BTreeMap::new(),
26 global_prefixes: BTreeMap::new(),
27 base: None,
28 dry_run: false,
29 }
30 }
31 pub fn with_vars(mut self, vars: BTreeMap<String, String>) -> Self {
32 self.vars = vars;
33 self
34 }
35 pub fn with_prefixes(
36 mut self, prefixes: BTreeMap<String, String>, base: Option<String>,
37 ) -> Self {
38 self.global_prefixes = prefixes;
39 self.base = base;
40 self
41 }
42 pub fn dry(mut self, dry: bool) -> Self {
43 self.dry_run = dry;
44 self
45 }
46}
47
48pub struct Generator {
49 pub pipeline: Pipeline,
50 pub ctx: GenContext,
51}
52
53impl Generator {
54 pub fn new(pipeline: Pipeline, ctx: GenContext) -> Self {
55 Self { pipeline, ctx }
56 }
57
58 pub fn generate(&mut self) -> Result<PathBuf> {
59 let input = fs::read_to_string(&self.ctx.template_path)?;
60 let mut tmpl = Template::parse(&input)?;
61
62 let mut tctx = Context::from_serialize(&self.ctx.vars)?;
64 insert_env(&mut tctx);
65
66 tmpl.render_frontmatter(&mut self.pipeline.tera, &tctx)?;
68
69 tmpl.process_graph(&mut self.pipeline.graph, &mut self.pipeline.tera, &tctx, &self.ctx.template_path)?;
71
72 let rendered = tmpl.render(&mut self.pipeline.tera, &tctx)?;
74
75 let output_path = if let Some(to_path) = &tmpl.front.to {
77 let rendered_to = self.pipeline.tera.render_str(to_path, &tctx)?;
78 self.ctx.output_root.join(rendered_to)
79 } else {
80 let template_name = self
82 .ctx
83 .template_path
84 .file_stem()
85 .unwrap_or_default()
86 .to_string_lossy();
87 self.ctx.output_root.join(format!("{}.out", template_name))
88 };
89
90 if !self.ctx.dry_run {
91 if let Some(parent) = output_path.parent() {
93 fs::create_dir_all(parent)?;
94 }
95 fs::write(&output_path, rendered)?;
96 }
97
98 Ok(output_path)
99 }
100}
101
102fn insert_env(ctx: &mut Context) {
103 for (k, v) in env::vars() {
104 ctx.insert(&k, &v);
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use std::collections::BTreeMap;
112 use std::fs;
113 use tempfile::TempDir;
114
115 fn create_test_pipeline() -> Pipeline {
116 Pipeline::new().unwrap()
117 }
118
119 fn create_test_template(content: &str) -> (TempDir, PathBuf) {
120 let temp_dir = TempDir::new().expect("Failed to create temp dir");
121 let template_path = temp_dir.path().join("test.tmpl");
122 fs::write(&template_path, content).expect("Failed to write test template");
123 (temp_dir, template_path)
124 }
125
126 #[test]
127 fn test_gen_context_new() {
128 let template_path = PathBuf::from("test.tmpl");
129 let output_root = PathBuf::from("output");
130
131 let ctx = GenContext::new(template_path.clone(), output_root.clone());
132
133 assert_eq!(ctx.template_path, template_path);
134 assert_eq!(ctx.output_root, output_root);
135 assert!(ctx.vars.is_empty());
136 assert!(ctx.global_prefixes.is_empty());
137 assert!(ctx.base.is_none());
138 assert!(!ctx.dry_run);
139 }
140
141 #[test]
142 fn test_gen_context_with_vars() {
143 let template_path = PathBuf::from("test.tmpl");
144 let output_root = PathBuf::from("output");
145 let mut vars = BTreeMap::new();
146 vars.insert("name".to_string(), "TestApp".to_string());
147 vars.insert("version".to_string(), "1.0.0".to_string());
148
149 let ctx = GenContext::new(template_path, output_root).with_vars(vars.clone());
150
151 assert_eq!(ctx.vars, vars);
152 }
153
154 #[test]
155 fn test_gen_context_with_prefixes() {
156 let template_path = PathBuf::from("test.tmpl");
157 let output_root = PathBuf::from("output");
158 let mut prefixes = BTreeMap::new();
159 prefixes.insert("ex".to_string(), "http://example.org/".to_string());
160 let base = Some("http://example.org/base/".to_string());
161
162 let ctx = GenContext::new(template_path, output_root)
163 .with_prefixes(prefixes.clone(), base.clone());
164
165 assert_eq!(ctx.global_prefixes, prefixes);
166 assert_eq!(ctx.base, base);
167 }
168
169 #[test]
170 fn test_gen_context_dry() {
171 let template_path = PathBuf::from("test.tmpl");
172 let output_root = PathBuf::from("output");
173
174 let ctx = GenContext::new(template_path, output_root).dry(true);
175 assert!(ctx.dry_run);
176
177 let ctx = GenContext::new(PathBuf::from("test.tmpl"), PathBuf::from("output")).dry(false);
178 assert!(!ctx.dry_run);
179 }
180
181 #[test]
182 fn test_generator_new() {
183 let pipeline = create_test_pipeline();
184 let template_path = PathBuf::from("test.tmpl");
185 let output_root = PathBuf::from("output");
186 let ctx = GenContext::new(template_path, output_root);
187
188 let generator = Generator::new(pipeline, ctx);
189
190 assert!(generator
192 .ctx
193 .template_path
194 .to_string_lossy()
195 .contains("test.tmpl"));
196 assert!(generator
197 .ctx
198 .output_root
199 .to_string_lossy()
200 .contains("output"));
201 }
202
203 #[test]
204 fn test_generate_simple_template() {
205 let (_temp_dir, template_path) = create_test_template(
206 r#"---
207to: "output/{{ name | lower }}.rs"
208---
209// Generated by rgen
210// Name: {{ name }}
211// Description: {{ description }}
212"#,
213 );
214
215 let output_dir = _temp_dir.path();
216 let pipeline = create_test_pipeline();
217 let mut vars = BTreeMap::new();
218 vars.insert("name".to_string(), "MyApp".to_string());
219 vars.insert("description".to_string(), "A test application".to_string());
220
221 let ctx = GenContext::new(template_path, output_dir.to_path_buf()).with_vars(vars);
222
223 let mut generator = Generator::new(pipeline, ctx);
224 let result = generator.generate();
225
226 if let Err(e) = &result {
227 eprintln!("Generation failed: {}", e);
228 }
229 assert!(result.is_ok());
230 let output_path = result.unwrap();
231 assert_eq!(output_path, output_dir.join("output/myapp.rs"));
232
233 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
235 assert!(content.contains("// Generated by rgen"));
236 assert!(content.contains("// Name: MyApp"));
237 assert!(content.contains("// Description: A test application"));
238 }
239
240 #[test]
241 fn test_generate_dry_run() {
242 let (_temp_dir, template_path) = create_test_template(
243 r#"---
244to: "output/{{ name | lower }}.rs"
245---
246// Generated content
247"#,
248 );
249
250 let output_dir = _temp_dir.path();
251 let pipeline = create_test_pipeline();
252 let mut vars = BTreeMap::new();
253 vars.insert("name".to_string(), "MyApp".to_string());
254
255 let ctx = GenContext::new(template_path, output_dir.to_path_buf())
256 .with_vars(vars)
257 .dry(true);
258
259 let mut generator = Generator::new(pipeline, ctx);
260 let result = generator.generate();
261
262 assert!(result.is_ok());
263 let output_path = result.unwrap();
264 assert_eq!(output_path, output_dir.join("output/myapp.rs"));
265
266 assert!(!output_path.exists());
268 }
269
270 #[test]
271 fn test_generate_with_default_output() {
272 let (_temp_dir, template_path) = create_test_template(
273 r#"---
274{}
275---
276// Default output content
277"#,
278 );
279
280 let output_dir = _temp_dir.path();
281 let pipeline = create_test_pipeline();
282 let ctx = GenContext::new(template_path, output_dir.to_path_buf());
283
284 let mut generator = Generator::new(pipeline, ctx);
285 let result = generator.generate();
286
287 if let Err(e) = &result {
288 eprintln!("Generation failed: {}", e);
289 }
290 assert!(result.is_ok());
291 let output_path = result.unwrap();
292 assert_eq!(output_path, output_dir.join("test.out"));
293
294 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
296 assert!(content.contains("// Default output content"));
297 }
298
299 #[test]
300 fn test_generate_with_nested_output_path() {
301 let (_temp_dir, template_path) = create_test_template(
302 r#"---
303to: "src/{{ module }}/{{ name | lower }}.rs"
304---
305// Nested output content
306"#,
307 );
308
309 let output_dir = _temp_dir.path();
310 let pipeline = create_test_pipeline();
311 let mut vars = BTreeMap::new();
312 vars.insert("name".to_string(), "MyModule".to_string());
313 vars.insert("module".to_string(), "handlers".to_string());
314
315 let ctx = GenContext::new(template_path, output_dir.to_path_buf()).with_vars(vars);
316
317 let mut generator = Generator::new(pipeline, ctx);
318 let result = generator.generate();
319
320 assert!(result.is_ok());
321 let output_path = result.unwrap();
322 assert_eq!(output_path, output_dir.join("src/handlers/mymodule.rs"));
323
324 assert!(output_path.exists());
326 let content = fs::read_to_string(&output_path).expect("Failed to read output file");
327 assert!(content.contains("// Nested output content"));
328 }
329
330 #[test]
331 fn test_generate_invalid_template() {
332 let (_temp_dir, template_path) = create_test_template(
333 r#"---
334invalid_yaml: [unclosed
335---
336// Template body
337"#,
338 );
339
340 let output_dir = _temp_dir.path();
341 let pipeline = create_test_pipeline();
342 let ctx = GenContext::new(template_path, output_dir.to_path_buf());
343
344 let mut generator = Generator::new(pipeline, ctx);
345 let result = generator.generate();
346
347 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_generate_missing_template_file() {
353 let temp_dir = TempDir::new().expect("Failed to create temp dir");
354 let template_path = temp_dir.path().join("nonexistent.tmpl");
355 let output_dir = temp_dir.path();
356
357 let pipeline = create_test_pipeline();
358 let ctx = GenContext::new(template_path, output_dir.to_path_buf());
359
360 let mut generator = Generator::new(pipeline, ctx);
361 let result = generator.generate();
362
363 assert!(result.is_err());
365 }
366
367 #[test]
368 fn test_insert_env() {
369 let mut ctx = Context::new();
370
371 std::env::set_var("TEST_RGEN_VAR", "test_value");
373
374 insert_env(&mut ctx);
375
376 assert!(ctx.get("TEST_RGEN_VAR").is_some());
378
379 std::env::remove_var("TEST_RGEN_VAR");
381 }
382}