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 FileRules {
123    /// Rules for generated files that are always overwritten.
124    pub fn always_overwrite() -> Self {
125        Self {
126            overwrite: Overwrite::Always,
127            header: None,
128        }
129    }
130
131    /// Rules for user-editable files that are only created if missing.
132    pub fn create_once() -> Self {
133        Self {
134            overwrite: Overwrite::IfMissing,
135            header: None,
136        }
137    }
138
139    /// Set the header marker for this file.
140    pub fn with_header(mut self, header: &'static str) -> Self {
141        self.header = Some(header);
142        self
143    }
144}
145
146impl Default for FileRules {
147    fn default() -> Self {
148        Self::always_overwrite()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::fs;
155
156    use tempfile::TempDir;
157
158    use super::*;
159
160    #[test]
161    fn test_write_file_creates_file() {
162        let temp = TempDir::new().unwrap();
163        let path = temp.path().join("test.txt");
164
165        write_file(&path, "hello").unwrap();
166
167        assert!(path.exists());
168        assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
169    }
170
171    #[test]
172    fn test_write_file_creates_parent_dirs() {
173        let temp = TempDir::new().unwrap();
174        let path = temp.path().join("a").join("b").join("c").join("test.txt");
175
176        write_file(&path, "nested").unwrap();
177
178        assert!(path.exists());
179        assert_eq!(fs::read_to_string(&path).unwrap(), "nested");
180    }
181
182    #[test]
183    fn test_write_file_overwrites_existing() {
184        let temp = TempDir::new().unwrap();
185        let path = temp.path().join("test.txt");
186
187        write_file(&path, "first").unwrap();
188        write_file(&path, "second").unwrap();
189
190        assert_eq!(fs::read_to_string(&path).unwrap(), "second");
191    }
192
193    #[test]
194    fn test_file_write_always_overwrites() {
195        let temp = TempDir::new().unwrap();
196        let path = temp.path().join("test.txt");
197
198        fs::write(&path, "original").unwrap();
199
200        let file = File::new(&path, "updated");
201        let result = file.write().unwrap();
202
203        assert_eq!(result, WriteResult::Written);
204        assert_eq!(fs::read_to_string(&path).unwrap(), "updated");
205    }
206
207    #[test]
208    fn test_file_write_if_missing_creates_new() {
209        let temp = TempDir::new().unwrap();
210        let path = temp.path().join("new.txt");
211
212        let file = File {
213            path: path.clone(),
214            content: "new content".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::Written);
223        assert_eq!(fs::read_to_string(&path).unwrap(), "new content");
224    }
225
226    #[test]
227    fn test_file_write_if_missing_skips_existing() {
228        let temp = TempDir::new().unwrap();
229        let path = temp.path().join("existing.txt");
230
231        fs::write(&path, "original").unwrap();
232
233        let file = File {
234            path: path.clone(),
235            content: "should not write".to_string(),
236            rules: FileRules {
237                overwrite: Overwrite::IfMissing,
238                header: None,
239            },
240        };
241        let result = file.write().unwrap();
242
243        assert_eq!(result, WriteResult::Skipped);
244        assert_eq!(fs::read_to_string(&path).unwrap(), "original");
245    }
246
247    #[test]
248    fn test_file_exists() {
249        let temp = TempDir::new().unwrap();
250        let path = temp.path().join("test.txt");
251
252        let file = File::new(&path, "content");
253        assert!(!file.exists());
254
255        fs::write(&path, "content").unwrap();
256        assert!(file.exists());
257    }
258}