cuenv_cubes/
generator.rs

1//! File generation engine
2//!
3//! This module handles the core file generation logic, including:
4//! - Writing files based on mode (managed vs scaffold)
5//! - Formatting generated code
6//! - Checking if files need updates
7
8use crate::cube::{Cube, FileMode};
9use crate::formatter::Formatter;
10use crate::{CodegenError, Result};
11use std::path::{Path, PathBuf};
12
13/// Generated file information
14#[derive(Debug, Clone)]
15pub struct GeneratedFile {
16    /// Path where the file was/will be written
17    pub path: PathBuf,
18    /// Final content (after formatting)
19    pub content: String,
20    /// Generation mode
21    pub mode: FileMode,
22    /// Programming language
23    pub language: String,
24}
25
26/// Options for file generation
27#[derive(Debug, Clone)]
28pub struct GenerateOptions {
29    /// Output directory for generated files
30    pub output_dir: PathBuf,
31    /// Check mode: don't write files, just check if they would change
32    pub check: bool,
33    /// Show diffs for changed files
34    pub diff: bool,
35}
36
37impl Default for GenerateOptions {
38    fn default() -> Self {
39        Self {
40            output_dir: PathBuf::from("."),
41            check: false,
42            diff: false,
43        }
44    }
45}
46
47/// File generator
48#[derive(Debug)]
49pub struct Generator {
50    cube: Cube,
51    formatter: Formatter,
52}
53
54impl Generator {
55    /// Create a new generator from a cube
56    #[must_use]
57    pub fn new(cube: Cube) -> Self {
58        Self {
59            cube,
60            formatter: Formatter::new(),
61        }
62    }
63
64    /// Generate all files from the cube
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if file writing or formatting fails
69    pub fn generate(&self, options: &GenerateOptions) -> Result<Vec<GeneratedFile>> {
70        let mut generated_files = Vec::new();
71
72        for (file_path, file_def) in self.cube.files() {
73            let output_path = options.output_dir.join(file_path);
74
75            // Format the content
76            let formatted_content =
77                self.formatter
78                    .format(&file_def.content, &file_def.language, &file_def.format)?;
79
80            let generated = GeneratedFile {
81                path: output_path.clone(),
82                content: formatted_content.clone(),
83                mode: file_def.mode,
84                language: file_def.language.clone(),
85            };
86
87            // Handle different modes
88            match file_def.mode {
89                FileMode::Managed => {
90                    if options.check {
91                        self.check_file(&output_path, &formatted_content)?;
92                    } else {
93                        self.write_file(&output_path, &formatted_content)?;
94                    }
95                }
96                FileMode::Scaffold => {
97                    if output_path.exists() {
98                        tracing::info!("Skipping {} (scaffold mode, file exists)", file_path);
99                    } else if options.check {
100                        return Err(CodegenError::Generation(format!(
101                            "Missing scaffold file: {file_path}"
102                        )));
103                    } else {
104                        self.write_file(&output_path, &formatted_content)?;
105                    }
106                }
107            }
108
109            generated_files.push(generated);
110        }
111
112        Ok(generated_files)
113    }
114
115    /// Write a file to disk
116    #[allow(clippy::unused_self)] // Will use self for write options in future
117    fn write_file(&self, path: &Path, content: &str) -> Result<()> {
118        // Create parent directories if they don't exist
119        if let Some(parent) = path.parent() {
120            std::fs::create_dir_all(parent)?;
121        }
122
123        std::fs::write(path, content)?;
124        tracing::info!("Generated: {}", path.display());
125
126        Ok(())
127    }
128
129    /// Check if a file would be modified
130    #[allow(clippy::unused_self)] // Will use self for check options in future
131    fn check_file(&self, path: &Path, expected_content: &str) -> Result<()> {
132        if !path.exists() {
133            return Err(CodegenError::Generation(format!(
134                "Missing managed file: {}",
135                path.display()
136            )));
137        }
138
139        let actual_content = std::fs::read_to_string(path)?;
140
141        if actual_content != expected_content {
142            return Err(CodegenError::Generation(format!(
143                "File would be modified: {}",
144                path.display()
145            )));
146        }
147
148        Ok(())
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::cube::{CubeData, FormatConfig, ProjectFileDefinition};
156    use std::collections::HashMap;
157    use tempfile::TempDir;
158
159    fn create_test_cube() -> Cube {
160        let mut files = HashMap::new();
161        files.insert(
162            "test.json".to_string(),
163            ProjectFileDefinition {
164                content: r#"{"name":"test"}"#.to_string(),
165                language: "json".to_string(),
166                mode: FileMode::Managed,
167                format: FormatConfig::default(),
168                gitignore: false,
169            },
170        );
171
172        let data = CubeData {
173            files,
174            context: serde_json::Value::Null,
175        };
176
177        Cube {
178            data,
179            source_path: PathBuf::from("test.cue"),
180        }
181    }
182
183    #[test]
184    fn test_generator_new() {
185        let cube = create_test_cube();
186        let generator = Generator::new(cube);
187        assert!(generator.cube.files().contains_key("test.json"));
188    }
189
190    #[test]
191    fn test_generate_managed_file() {
192        let cube = create_test_cube();
193        let generator = Generator::new(cube);
194
195        let temp_dir = TempDir::new().unwrap();
196        let options = GenerateOptions {
197            output_dir: temp_dir.path().to_path_buf(),
198            check: false,
199            diff: false,
200        };
201
202        let result = generator.generate(&options);
203        assert!(result.is_ok());
204
205        let generated = result.unwrap();
206        assert_eq!(generated.len(), 1);
207        assert_eq!(generated[0].mode, FileMode::Managed);
208
209        let file_path = temp_dir.path().join("test.json");
210        assert!(file_path.exists());
211    }
212}