1use std::path::{Path, PathBuf};
2
3use eyre::Result;
4
5pub trait GeneratedFile {
7 fn path(&self, base: &Path) -> PathBuf;
9
10 fn rules(&self) -> FileRules;
12
13 fn render(&self) -> String;
15
16 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum WriteResult {
49 Written,
51 Skipped,
53}
54
55pub struct File {
57 path: PathBuf,
58 content: String,
59 rules: FileRules,
60}
61
62impl File {
63 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 pub fn path(&self) -> &Path {
74 &self.path
75 }
76
77 pub fn content(&self) -> &str {
79 &self.content
80 }
81
82 pub fn exists(&self) -> bool {
84 self.path.exists()
85 }
86
87 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#[derive(Debug, Clone)]
108pub struct FileRules {
109 pub overwrite: Overwrite,
110 pub header: Option<&'static str>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum Overwrite {
116 Always,
118 IfMissing,
120}
121
122impl FileRules {
123 pub fn always_overwrite() -> Self {
125 Self {
126 overwrite: Overwrite::Always,
127 header: None,
128 }
129 }
130
131 pub fn create_once() -> Self {
133 Self {
134 overwrite: Overwrite::IfMissing,
135 header: None,
136 }
137 }
138
139 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}