baobao_codegen/generation/
registry.rs1use std::path::{Path, PathBuf};
26
27use baobao_core::{FileRules, Overwrite, WriteResult};
28use eyre::Result;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
32pub enum FileCategory {
33 Config,
36 Infrastructure,
39 Generated,
42 Handler,
45}
46
47impl FileCategory {
48 pub fn default_overwrite(&self) -> Overwrite {
50 match self {
51 FileCategory::Handler => Overwrite::IfMissing,
52 _ => Overwrite::Always,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct FileEntry {
60 pub path: String,
62 pub content: String,
64 pub category: FileCategory,
66 pub overwrite: Option<Overwrite>,
68}
69
70impl FileEntry {
71 pub fn new(
73 path: impl Into<String>,
74 content: impl Into<String>,
75 category: FileCategory,
76 ) -> Self {
77 Self {
78 path: path.into(),
79 content: content.into(),
80 category,
81 overwrite: None,
82 }
83 }
84
85 pub fn config(path: impl Into<String>, content: impl Into<String>) -> Self {
87 Self::new(path, content, FileCategory::Config)
88 }
89
90 pub fn infrastructure(path: impl Into<String>, content: impl Into<String>) -> Self {
92 Self::new(path, content, FileCategory::Infrastructure)
93 }
94
95 pub fn generated(path: impl Into<String>, content: impl Into<String>) -> Self {
97 Self::new(path, content, FileCategory::Generated)
98 }
99
100 pub fn handler(path: impl Into<String>, content: impl Into<String>) -> Self {
102 Self::new(path, content, FileCategory::Handler)
103 }
104
105 pub fn with_overwrite(mut self, overwrite: Overwrite) -> Self {
107 self.overwrite = Some(overwrite);
108 self
109 }
110
111 pub fn overwrite(&self) -> Overwrite {
113 self.overwrite
114 .unwrap_or_else(|| self.category.default_overwrite())
115 }
116
117 pub fn rules(&self) -> FileRules {
119 FileRules {
120 overwrite: self.overwrite(),
121 header: None,
122 }
123 }
124
125 pub fn full_path(&self, base: &Path) -> PathBuf {
127 base.join(&self.path)
128 }
129
130 pub fn write(&self, base: &Path) -> Result<WriteResult> {
132 let path = self.full_path(base);
133 let overwrite = self.overwrite();
134
135 match overwrite {
136 Overwrite::Always => {
137 write_file(&path, &self.content)?;
138 Ok(WriteResult::Written)
139 }
140 Overwrite::IfMissing => {
141 if path.exists() {
142 Ok(WriteResult::Skipped)
143 } else {
144 write_file(&path, &self.content)?;
145 Ok(WriteResult::Written)
146 }
147 }
148 }
149 }
150}
151
152#[derive(Debug, Default)]
157pub struct FileRegistry {
158 entries: Vec<FileEntry>,
159}
160
161impl FileRegistry {
162 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn register(&mut self, entry: FileEntry) {
169 self.entries.push(entry);
170 }
171
172 pub fn register_all(&mut self, entries: impl IntoIterator<Item = FileEntry>) {
174 self.entries.extend(entries);
175 }
176
177 pub fn entries(&self) -> impl Iterator<Item = &FileEntry> {
179 let mut sorted: Vec<_> = self.entries.iter().collect();
180 sorted.sort_by_key(|e| e.category);
181 sorted.into_iter()
182 }
183
184 pub fn entries_by_category(&self, category: FileCategory) -> impl Iterator<Item = &FileEntry> {
186 self.entries.iter().filter(move |e| e.category == category)
187 }
188
189 pub fn len(&self) -> usize {
191 self.entries.len()
192 }
193
194 pub fn is_empty(&self) -> bool {
196 self.entries.is_empty()
197 }
198
199 pub fn preview(&self) -> Vec<PreviewEntry> {
201 self.entries()
202 .map(|e| PreviewEntry {
203 path: e.path.clone(),
204 content: e.content.clone(),
205 category: e.category,
206 })
207 .collect()
208 }
209
210 pub fn write_all(&self, base: &Path) -> Result<WriteStats> {
214 let mut stats = WriteStats::default();
215
216 for entry in self.entries() {
217 match entry.write(base)? {
218 WriteResult::Written => {
219 stats.written += 1;
220 stats.written_paths.push(entry.path.clone());
221 }
222 WriteResult::Skipped => {
223 stats.skipped += 1;
224 stats.skipped_paths.push(entry.path.clone());
225 }
226 }
227 }
228
229 Ok(stats)
230 }
231
232 pub fn clear(&mut self) {
234 self.entries.clear();
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct PreviewEntry {
241 pub path: String,
243 pub content: String,
245 pub category: FileCategory,
247}
248
249#[derive(Debug, Default)]
251pub struct WriteStats {
252 pub written: usize,
254 pub skipped: usize,
256 pub written_paths: Vec<String>,
258 pub skipped_paths: Vec<String>,
260}
261
262impl WriteStats {
263 pub fn total(&self) -> usize {
265 self.written + self.skipped
266 }
267}
268
269fn write_file(path: &Path, content: &str) -> Result<()> {
270 if let Some(parent) = path.parent() {
271 std::fs::create_dir_all(parent)?;
272 }
273 std::fs::write(path, content)?;
274 Ok(())
275}
276
277#[cfg(test)]
278mod tests {
279 use tempfile::TempDir;
280
281 use super::*;
282
283 #[test]
284 fn test_file_entry_categories() {
285 let config = FileEntry::config("Cargo.toml", "");
286 assert_eq!(config.category, FileCategory::Config);
287 assert_eq!(config.overwrite(), Overwrite::Always);
288
289 let handler = FileEntry::handler("src/handlers/hello.rs", "");
290 assert_eq!(handler.category, FileCategory::Handler);
291 assert_eq!(handler.overwrite(), Overwrite::IfMissing);
292 }
293
294 #[test]
295 fn test_registry_ordering() {
296 let mut registry = FileRegistry::new();
297
298 registry.register(FileEntry::handler("handler.rs", ""));
300 registry.register(FileEntry::config("config.toml", ""));
301 registry.register(FileEntry::generated("generated.rs", ""));
302 registry.register(FileEntry::infrastructure("main.rs", ""));
303
304 let paths: Vec<_> = registry.entries().map(|e| e.path.as_str()).collect();
306 assert_eq!(
307 paths,
308 vec!["config.toml", "main.rs", "generated.rs", "handler.rs"]
309 );
310 }
311
312 #[test]
313 fn test_registry_write_all() {
314 let temp = TempDir::new().unwrap();
315 let mut registry = FileRegistry::new();
316
317 registry.register(FileEntry::config("test.txt", "content"));
318 registry.register(FileEntry::handler("stub.txt", "stub"));
319
320 let stats = registry.write_all(temp.path()).unwrap();
321
322 assert_eq!(stats.written, 2);
323 assert_eq!(stats.skipped, 0);
324 assert!(temp.path().join("test.txt").exists());
325 assert!(temp.path().join("stub.txt").exists());
326 }
327
328 #[test]
329 fn test_handler_skipped_if_exists() {
330 let temp = TempDir::new().unwrap();
331 let path = temp.path().join("handler.rs");
332
333 std::fs::write(&path, "user code").unwrap();
335
336 let mut registry = FileRegistry::new();
337 registry.register(FileEntry::handler("handler.rs", "stub"));
338
339 let stats = registry.write_all(temp.path()).unwrap();
340
341 assert_eq!(stats.written, 0);
342 assert_eq!(stats.skipped, 1);
343 assert_eq!(std::fs::read_to_string(&path).unwrap(), "user code");
345 }
346
347 #[test]
348 fn test_preview() {
349 let mut registry = FileRegistry::new();
350 registry.register(FileEntry::config("a.txt", "content a"));
351 registry.register(FileEntry::generated("b.txt", "content b"));
352
353 let preview = registry.preview();
354
355 assert_eq!(preview.len(), 2);
356 assert_eq!(preview[0].path, "a.txt");
357 assert_eq!(preview[1].path, "b.txt");
358 }
359}