baobao_core/
file.rs

1use std::path::{Path, PathBuf};
2
3use eyre::Result;
4
5/// Trait for types that represent a generated file
6pub trait GeneratedFile {
7    /// Get the file path relative to the base directory
8    fn path(&self, base: &Path) -> PathBuf;
9
10    /// Get the rules for writing this file
11    fn rules(&self) -> FileRules;
12
13    /// Render the file content
14    fn render(&self) -> String;
15
16    /// Write the file to disk
17    fn write(&self, base: &Path) -> Result<WriteResult> {
18        let path = self.path(base);
19        let rules = self.rules();
20
21        match rules.overwrite {
22            Overwrite::Always => {
23                write_file(&path, &self.render())?;
24                Ok(WriteResult::Written)
25            }
26            Overwrite::IfMissing => {
27                if path.exists() {
28                    Ok(WriteResult::Skipped)
29                } else {
30                    write_file(&path, &self.render())?;
31                    Ok(WriteResult::Written)
32                }
33            }
34        }
35    }
36}
37
38fn write_file(path: &Path, content: &str) -> Result<()> {
39    if let Some(parent) = path.parent() {
40        std::fs::create_dir_all(parent)?;
41    }
42    std::fs::write(path, content)?;
43    Ok(())
44}
45
46/// Result of a write operation
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum WriteResult {
49    /// File was written
50    Written,
51    /// File was skipped (already exists)
52    Skipped,
53}
54
55/// A file to be generated
56pub struct File {
57    path: PathBuf,
58    content: String,
59    rules: FileRules,
60}
61
62impl File {
63    /// Create a new file with the given path and content (default rules: always overwrite)
64    pub fn new(path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
65        Self {
66            path: path.into(),
67            content: content.into(),
68            rules: FileRules::default(),
69        }
70    }
71
72    /// Get the file path
73    pub fn path(&self) -> &Path {
74        &self.path
75    }
76
77    /// Get the file content
78    pub fn content(&self) -> &str {
79        &self.content
80    }
81
82    /// Check if the file exists
83    pub fn exists(&self) -> bool {
84        self.path.exists()
85    }
86
87    /// Write the file according to its rules
88    pub fn write(&self) -> Result<WriteResult> {
89        match self.rules.overwrite {
90            Overwrite::Always => {
91                write_file(&self.path, &self.content)?;
92                Ok(WriteResult::Written)
93            }
94            Overwrite::IfMissing => {
95                if self.exists() {
96                    Ok(WriteResult::Skipped)
97                } else {
98                    write_file(&self.path, &self.content)?;
99                    Ok(WriteResult::Written)
100                }
101            }
102        }
103    }
104}
105
106/// Rules that determine how a file should be written
107#[derive(Debug, Clone)]
108pub struct FileRules {
109    pub overwrite: Overwrite,
110    pub header: Option<&'static str>,
111}
112
113/// How to handle existing files
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Overwrite {
116    /// Always overwrite (generated code)
117    Always,
118    /// Only create if file doesn't exist (stubs)
119    IfMissing,
120}
121
122impl Default for FileRules {
123    fn default() -> Self {
124        Self {
125            overwrite: Overwrite::Always,
126            header: None,
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use std::fs;
134
135    use tempfile::TempDir;
136
137    use super::*;
138
139    #[test]
140    fn test_write_file_creates_file() {
141        let temp = TempDir::new().unwrap();
142        let path = temp.path().join("test.txt");
143
144        write_file(&path, "hello").unwrap();
145
146        assert!(path.exists());
147        assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
148    }
149
150    #[test]
151    fn test_write_file_creates_parent_dirs() {
152        let temp = TempDir::new().unwrap();
153        let path = temp.path().join("a").join("b").join("c").join("test.txt");
154
155        write_file(&path, "nested").unwrap();
156
157        assert!(path.exists());
158        assert_eq!(fs::read_to_string(&path).unwrap(), "nested");
159    }
160
161    #[test]
162    fn test_write_file_overwrites_existing() {
163        let temp = TempDir::new().unwrap();
164        let path = temp.path().join("test.txt");
165
166        write_file(&path, "first").unwrap();
167        write_file(&path, "second").unwrap();
168
169        assert_eq!(fs::read_to_string(&path).unwrap(), "second");
170    }
171
172    #[test]
173    fn test_file_write_always_overwrites() {
174        let temp = TempDir::new().unwrap();
175        let path = temp.path().join("test.txt");
176
177        fs::write(&path, "original").unwrap();
178
179        let file = File::new(&path, "updated");
180        let result = file.write().unwrap();
181
182        assert_eq!(result, WriteResult::Written);
183        assert_eq!(fs::read_to_string(&path).unwrap(), "updated");
184    }
185
186    #[test]
187    fn test_file_write_if_missing_creates_new() {
188        let temp = TempDir::new().unwrap();
189        let path = temp.path().join("new.txt");
190
191        let file = File {
192            path: path.clone(),
193            content: "new content".to_string(),
194            rules: FileRules {
195                overwrite: Overwrite::IfMissing,
196                header: None,
197            },
198        };
199        let result = file.write().unwrap();
200
201        assert_eq!(result, WriteResult::Written);
202        assert_eq!(fs::read_to_string(&path).unwrap(), "new content");
203    }
204
205    #[test]
206    fn test_file_write_if_missing_skips_existing() {
207        let temp = TempDir::new().unwrap();
208        let path = temp.path().join("existing.txt");
209
210        fs::write(&path, "original").unwrap();
211
212        let file = File {
213            path: path.clone(),
214            content: "should not write".to_string(),
215            rules: FileRules {
216                overwrite: Overwrite::IfMissing,
217                header: None,
218            },
219        };
220        let result = file.write().unwrap();
221
222        assert_eq!(result, WriteResult::Skipped);
223        assert_eq!(fs::read_to_string(&path).unwrap(), "original");
224    }
225
226    #[test]
227    fn test_file_exists() {
228        let temp = TempDir::new().unwrap();
229        let path = temp.path().join("test.txt");
230
231        let file = File::new(&path, "content");
232        assert!(!file.exists());
233
234        fs::write(&path, "content").unwrap();
235        assert!(file.exists());
236    }
237}