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 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}