rgen_core/
generator.rs

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        // Context
63        let mut tctx = Context::from_serialize(&self.ctx.vars)?;
64        insert_env(&mut tctx);
65
66        // Render frontmatter
67        tmpl.render_frontmatter(&mut self.pipeline.tera, &tctx)?;
68
69        // Process graph
70        tmpl.process_graph(&mut self.pipeline.graph, &mut self.pipeline.tera, &tctx, &self.ctx.template_path)?;
71
72        // Render body
73        let rendered = tmpl.render(&mut self.pipeline.tera, &tctx)?;
74
75        // Determine output path
76        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            // Default to template name with .out extension
81            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            // Ensure parent directory exists
92            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        // Generator should be created successfully
191        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        // Verify file was created and has correct content
234        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        // Verify file was NOT created in dry run
267        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        // Verify file was created
295        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        // Verify directory was created and file exists
325        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        // Should fail due to invalid YAML in frontmatter
348        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        // Should fail due to missing template file
364        assert!(result.is_err());
365    }
366
367    #[test]
368    fn test_insert_env() {
369        let mut ctx = Context::new();
370
371        // Set a test environment variable
372        std::env::set_var("TEST_RGEN_VAR", "test_value");
373
374        insert_env(&mut ctx);
375
376        // Verify environment variable was inserted
377        assert!(ctx.get("TEST_RGEN_VAR").is_some());
378
379        // Clean up
380        std::env::remove_var("TEST_RGEN_VAR");
381    }
382}