Skip to main content

romance_core/generator/
plan.rs

1use anyhow::Result;
2use std::path::PathBuf;
3
4/// A marker that should exist in a file — used for pre-validation.
5pub struct MarkerCheck {
6    pub path: PathBuf,
7    pub marker: String,
8}
9
10/// Create a `MarkerCheck` from a path and marker string.
11pub fn check(path: impl Into<PathBuf>, marker: &str) -> MarkerCheck {
12    MarkerCheck {
13        path: path.into(),
14        marker: marker.to_string(),
15    }
16}
17
18/// Validate that all expected markers exist in their respective files.
19///
20/// Returns `Err` listing all missing markers if any are absent.
21pub 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
54/// Tracks newly created files during generation for rollback on failure.
55pub 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    /// Record a file that was created during generation.
67    pub fn track(&mut self, path: PathBuf) {
68        self.created_files.push(path);
69    }
70
71    /// Delete all tracked files (best-effort rollback).
72    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        // Don't create the file — simulate it being deleted already
172
173        let mut tracker = GenerationTracker::new();
174        tracker.track(file_path.clone());
175        // Should not panic
176        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        // Both missing markers should be listed
193        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}