baobao_codegen/generation/
registry.rs

1//! File registration pattern for declarative code generation.
2//!
3//! This module provides a registry-based approach to file generation that:
4//! - Makes file generation declarative
5//! - Centralizes file metadata (path, category, overwrite rules)
6//! - Enables dependency tracking between files
7//! - Simplifies preview and generation logic
8//!
9//! # Example
10//!
11//! ```ignore
12//! let mut registry = FileRegistry::new();
13//!
14//! // Register infrastructure files (always overwritten)
15//! registry.register(FileEntry::always("Cargo.toml", cargo_toml.render()));
16//! registry.register(FileEntry::always("src/main.rs", main_rs.render()));
17//!
18//! // Register handler stubs (only if missing)
19//! registry.register(FileEntry::if_missing("src/handlers/hello.rs", stub.render()));
20//!
21//! // Generate all files
22//! registry.write_all(&output_dir)?;
23//! ```
24
25use std::path::{Path, PathBuf};
26
27use baobao_core::{FileRules, Overwrite, WriteResult};
28use eyre::Result;
29
30/// Category of generated file, determining generation order and behavior.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
32pub enum FileCategory {
33    /// Project configuration files (Cargo.toml, package.json, etc.)
34    /// Generated first, always overwritten.
35    Config,
36    /// Core infrastructure files (main.rs, index.ts, etc.)
37    /// Generated second, always overwritten.
38    Infrastructure,
39    /// Generated code files (cli.rs, context.rs, commands/*.rs)
40    /// Generated third, always overwritten.
41    Generated,
42    /// Handler stub files that users edit.
43    /// Generated last, only if missing.
44    Handler,
45}
46
47impl FileCategory {
48    /// Get the default overwrite behavior for this category.
49    pub fn default_overwrite(&self) -> Overwrite {
50        match self {
51            FileCategory::Handler => Overwrite::IfMissing,
52            _ => Overwrite::Always,
53        }
54    }
55}
56
57/// An entry in the file registry representing a file to be generated.
58#[derive(Debug, Clone)]
59pub struct FileEntry {
60    /// Relative path from output directory.
61    pub path: String,
62    /// File content.
63    pub content: String,
64    /// Category determining generation behavior.
65    pub category: FileCategory,
66    /// Override default overwrite behavior.
67    pub overwrite: Option<Overwrite>,
68}
69
70impl FileEntry {
71    /// Create a new file entry with the given category.
72    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    /// Create a config file (always overwritten, generated first).
86    pub fn config(path: impl Into<String>, content: impl Into<String>) -> Self {
87        Self::new(path, content, FileCategory::Config)
88    }
89
90    /// Create an infrastructure file (always overwritten).
91    pub fn infrastructure(path: impl Into<String>, content: impl Into<String>) -> Self {
92        Self::new(path, content, FileCategory::Infrastructure)
93    }
94
95    /// Create a generated code file (always overwritten).
96    pub fn generated(path: impl Into<String>, content: impl Into<String>) -> Self {
97        Self::new(path, content, FileCategory::Generated)
98    }
99
100    /// Create a handler stub file (only if missing).
101    pub fn handler(path: impl Into<String>, content: impl Into<String>) -> Self {
102        Self::new(path, content, FileCategory::Handler)
103    }
104
105    /// Override the default overwrite behavior.
106    pub fn with_overwrite(mut self, overwrite: Overwrite) -> Self {
107        self.overwrite = Some(overwrite);
108        self
109    }
110
111    /// Get the effective overwrite behavior.
112    pub fn overwrite(&self) -> Overwrite {
113        self.overwrite
114            .unwrap_or_else(|| self.category.default_overwrite())
115    }
116
117    /// Get the file rules for this entry.
118    pub fn rules(&self) -> FileRules {
119        FileRules {
120            overwrite: self.overwrite(),
121            header: None,
122        }
123    }
124
125    /// Get the full path for this entry.
126    pub fn full_path(&self, base: &Path) -> PathBuf {
127        base.join(&self.path)
128    }
129
130    /// Write this file to disk.
131    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/// Registry for collecting and managing generated files.
153///
154/// Files are stored by category and generated in category order:
155/// Config -> Infrastructure -> Generated -> Handler
156#[derive(Debug, Default)]
157pub struct FileRegistry {
158    entries: Vec<FileEntry>,
159}
160
161impl FileRegistry {
162    /// Create a new empty registry.
163    pub fn new() -> Self {
164        Self::default()
165    }
166
167    /// Register a file entry.
168    pub fn register(&mut self, entry: FileEntry) {
169        self.entries.push(entry);
170    }
171
172    /// Register multiple file entries.
173    pub fn register_all(&mut self, entries: impl IntoIterator<Item = FileEntry>) {
174        self.entries.extend(entries);
175    }
176
177    /// Get all registered entries, sorted by category.
178    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    /// Get entries for a specific category.
185    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    /// Get the number of registered entries.
190    pub fn len(&self) -> usize {
191        self.entries.len()
192    }
193
194    /// Check if the registry is empty.
195    pub fn is_empty(&self) -> bool {
196        self.entries.is_empty()
197    }
198
199    /// Preview all files (returns path and content pairs).
200    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    /// Write all files to the output directory.
211    ///
212    /// Files are written in category order. Returns statistics about what was written.
213    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    /// Clear all registered entries.
233    pub fn clear(&mut self) {
234        self.entries.clear();
235    }
236}
237
238/// A preview entry for displaying what would be generated.
239#[derive(Debug, Clone)]
240pub struct PreviewEntry {
241    /// Relative path from output directory.
242    pub path: String,
243    /// File content.
244    pub content: String,
245    /// File category.
246    pub category: FileCategory,
247}
248
249/// Statistics from a write operation.
250#[derive(Debug, Default)]
251pub struct WriteStats {
252    /// Number of files written.
253    pub written: usize,
254    /// Number of files skipped (already existed).
255    pub skipped: usize,
256    /// Paths of written files.
257    pub written_paths: Vec<String>,
258    /// Paths of skipped files.
259    pub skipped_paths: Vec<String>,
260}
261
262impl WriteStats {
263    /// Total number of files processed.
264    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        // Register in random order
299        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        // Should come out in category order
305        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        // Create existing file
334        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        // Original content preserved
344        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}