romance_core/generator/
plan.rs1use anyhow::Result;
2use std::path::PathBuf;
3
4pub struct MarkerCheck {
6 pub path: PathBuf,
7 pub marker: String,
8}
9
10pub fn check(path: impl Into<PathBuf>, marker: &str) -> MarkerCheck {
12 MarkerCheck {
13 path: path.into(),
14 marker: marker.to_string(),
15 }
16}
17
18pub fn validate_markers(checks: &[MarkerCheck]) -> Result<()> {
22 let mut missing = Vec::new();
23
24 for c in checks {
25 if !c.path.exists() {
26 missing.push(format!(
27 "File '{}' does not exist (expected marker '{}')",
28 c.path.display(),
29 c.marker
30 ));
31 continue;
32 }
33 let content = std::fs::read_to_string(&c.path)?;
34 if !content.contains(&c.marker) {
35 missing.push(format!(
36 "Marker '{}' not found in '{}'",
37 c.marker,
38 c.path.display()
39 ));
40 }
41 }
42
43 if missing.is_empty() {
44 Ok(())
45 } else {
46 anyhow::bail!(
47 "Pre-validation failed — {} missing marker(s):\n {}",
48 missing.len(),
49 missing.join("\n ")
50 );
51 }
52}
53
54pub struct GenerationTracker {
56 created_files: Vec<PathBuf>,
57}
58
59impl GenerationTracker {
60 pub fn new() -> Self {
61 Self {
62 created_files: Vec::new(),
63 }
64 }
65
66 pub fn track(&mut self, path: PathBuf) {
68 self.created_files.push(path);
69 }
70
71 pub fn rollback(&self) {
73 for path in &self.created_files {
74 if path.exists() {
75 if let Err(e) = std::fs::remove_file(path) {
76 eprintln!(
77 " Warning: failed to clean up '{}': {}",
78 path.display(),
79 e
80 );
81 } else {
82 eprintln!(" Rolled back: {}", path.display());
83 }
84 }
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::io::Write;
93 use tempfile::NamedTempFile;
94
95 #[test]
96 fn validate_markers_all_present() {
97 let mut tmp = NamedTempFile::new().unwrap();
98 writeln!(tmp, "// === ROMANCE:MODS ===").unwrap();
99 writeln!(tmp, "// === ROMANCE:ROUTES ===").unwrap();
100 tmp.flush().unwrap();
101
102 let checks = vec![
103 check(tmp.path(), "// === ROMANCE:MODS ==="),
104 check(tmp.path(), "// === ROMANCE:ROUTES ==="),
105 ];
106 assert!(validate_markers(&checks).is_ok());
107 }
108
109 #[test]
110 fn validate_markers_missing_marker() {
111 let mut tmp = NamedTempFile::new().unwrap();
112 writeln!(tmp, "// some content").unwrap();
113 tmp.flush().unwrap();
114
115 let checks = vec![check(tmp.path(), "// === ROMANCE:MODS ===")];
116 let result = validate_markers(&checks);
117 assert!(result.is_err());
118 assert!(result.unwrap_err().to_string().contains("ROMANCE:MODS"));
119 }
120
121 #[test]
122 fn validate_markers_missing_file() {
123 let checks = vec![check(
124 std::path::Path::new("/tmp/romance_nonexistent_12345.rs"),
125 "// === ROMANCE:MODS ===",
126 )];
127 let result = validate_markers(&checks);
128 assert!(result.is_err());
129 assert!(result.unwrap_err().to_string().contains("does not exist"));
130 }
131
132 #[test]
133 fn generation_tracker_rollback() {
134 let dir = tempfile::tempdir().unwrap();
135 let file_path = dir.path().join("generated.rs");
136 std::fs::write(&file_path, "content").unwrap();
137 assert!(file_path.exists());
138
139 let mut tracker = GenerationTracker::new();
140 tracker.track(file_path.clone());
141 tracker.rollback();
142
143 assert!(!file_path.exists());
144 }
145
146 #[test]
147 fn generation_tracker_rollback_multiple_files() {
148 let dir = tempfile::tempdir().unwrap();
149 let file_a = dir.path().join("a.rs");
150 let file_b = dir.path().join("b.rs");
151 let file_c = dir.path().join("c.rs");
152 std::fs::write(&file_a, "a").unwrap();
153 std::fs::write(&file_b, "b").unwrap();
154 std::fs::write(&file_c, "c").unwrap();
155
156 let mut tracker = GenerationTracker::new();
157 tracker.track(file_a.clone());
158 tracker.track(file_b.clone());
159 tracker.track(file_c.clone());
160 tracker.rollback();
161
162 assert!(!file_a.exists());
163 assert!(!file_b.exists());
164 assert!(!file_c.exists());
165 }
166
167 #[test]
168 fn generation_tracker_rollback_already_deleted_file() {
169 let dir = tempfile::tempdir().unwrap();
170 let file_path = dir.path().join("gone.rs");
171 let mut tracker = GenerationTracker::new();
174 tracker.track(file_path.clone());
175 tracker.rollback();
177 }
178
179 #[test]
180 fn validate_markers_reports_all_missing() {
181 let mut tmp = NamedTempFile::new().unwrap();
182 writeln!(tmp, "// only this line").unwrap();
183 tmp.flush().unwrap();
184
185 let checks = vec![
186 check(tmp.path(), "// === ROMANCE:MODS ==="),
187 check(tmp.path(), "// === ROMANCE:ROUTES ==="),
188 ];
189 let result = validate_markers(&checks);
190 assert!(result.is_err());
191 let msg = result.unwrap_err().to_string();
192 assert!(msg.contains("ROMANCE:MODS"), "Error should mention MODS");
194 assert!(msg.contains("ROMANCE:ROUTES"), "Error should mention ROUTES");
195 assert!(msg.contains("2 missing marker(s)"));
196 }
197
198 #[test]
199 fn validate_markers_mixed_present_and_missing() {
200 let mut tmp = NamedTempFile::new().unwrap();
201 writeln!(tmp, "// === ROMANCE:MODS ===").unwrap();
202 tmp.flush().unwrap();
203
204 let checks = vec![
205 check(tmp.path(), "// === ROMANCE:MODS ==="),
206 check(tmp.path(), "// === ROMANCE:ROUTES ==="),
207 ];
208 let result = validate_markers(&checks);
209 assert!(result.is_err());
210 let msg = result.unwrap_err().to_string();
211 assert!(msg.contains("1 missing marker(s)"));
212 assert!(msg.contains("ROMANCE:ROUTES"));
213 assert!(!msg.contains("ROMANCE:MODS"));
214 }
215}